mirror of
https://github.com/libretro/RetroArch.git
synced 2026-11-12 09:21:35 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd97d63021 | ||
|
|
d7446f1609 | ||
|
|
baa6ff33c2 | ||
|
|
9e333a55eb | ||
|
|
5aaea68b34 | ||
|
|
28579236c9 |
No files matched your search
@@ -81,10 +81,6 @@ on:
|
||||
- 'libretro-common/include/retro_atomic.h'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
ios_sdk_only:
|
||||
description: 'Audit production VideoToolbox declarations against real iOS SDKs'
|
||||
type: boolean
|
||||
default: false
|
||||
native_aac_only:
|
||||
description: 'Run native AudioToolbox AAC acceptance without GPU/frontend jobs'
|
||||
type: boolean
|
||||
@@ -97,7 +93,7 @@ permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
stream-native:
|
||||
if: ${{ !inputs.ios_sdk_only && !inputs.native_aac_only && !inputs.frontend_stream_only }}
|
||||
if: ${{ !inputs.native_aac_only && !inputs.frontend_stream_only }}
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
@@ -156,7 +152,7 @@ jobs:
|
||||
${{ runner.temp }}/vt-metal-dispatch-mrc
|
||||
if-no-files-found: ignore
|
||||
aac-native:
|
||||
if: ${{ !inputs.ios_sdk_only && (inputs.native_aac_only || !inputs.frontend_stream_only) }}
|
||||
if: ${{ inputs.native_aac_only || !inputs.frontend_stream_only }}
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
@@ -188,7 +184,7 @@ jobs:
|
||||
path: ${{ runner.temp }}/audiotoolbox-src-evidence
|
||||
if-no-files-found: ignore
|
||||
encode:
|
||||
if: ${{ !inputs.ios_sdk_only && !inputs.native_aac_only && !inputs.frontend_stream_only }}
|
||||
if: ${{ !inputs.native_aac_only && !inputs.frontend_stream_only }}
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
@@ -289,7 +285,7 @@ jobs:
|
||||
if-no-files-found: warn
|
||||
|
||||
frontend-build:
|
||||
if: ${{ !inputs.ios_sdk_only && !inputs.native_aac_only && !inputs.frontend_stream_only }}
|
||||
if: ${{ !inputs.native_aac_only && !inputs.frontend_stream_only }}
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 40
|
||||
steps:
|
||||
@@ -326,7 +322,7 @@ jobs:
|
||||
if-no-files-found: warn
|
||||
|
||||
frontend-stream-build:
|
||||
if: ${{ !inputs.ios_sdk_only && !inputs.native_aac_only }}
|
||||
if: ${{ !inputs.native_aac_only }}
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
@@ -376,46 +372,9 @@ jobs:
|
||||
if-no-files-found: ignore
|
||||
|
||||
frontend-contracts:
|
||||
if: ${{ !inputs.ios_sdk_only && !inputs.native_aac_only }}
|
||||
if: ${{ !inputs.native_aac_only }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Source-clock capture and backpressure under sanitizers
|
||||
run: make -C samples/record/metal_frontend check SANITIZER=address,undefined
|
||||
|
||||
ios-sdk:
|
||||
if: ${{ inputs.ios_sdk_only }}
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Compile production VideoToolbox headers for iOS and simulator
|
||||
run: python3 samples/record/videotoolbox_encode/ios_sdk_check.py --output-dir "${{ runner.temp }}/vt-ios-sdk"
|
||||
- name: Save actual SDK diagnostics
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: vt-ios-sdk
|
||||
path: ${{ runner.temp }}/vt-ios-sdk
|
||||
if-no-files-found: error
|
||||
|
||||
ios-app:
|
||||
if: ${{ inputs.ios_sdk_only }}
|
||||
needs: ios-sdk
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Build and link actual iOS application with native recording
|
||||
run: python3 samples/record/videotoolbox_encode/ios_app_check.py --output-dir "${{ runner.temp }}/vt-ios-app"
|
||||
- name: Save full application build evidence
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: vt-ios-app
|
||||
path: |
|
||||
${{ runner.temp }}/vt-ios-app/*.json
|
||||
${{ runner.temp }}/vt-ios-app/*.log
|
||||
${{ runner.temp }}/vt-ios-app/*.txt
|
||||
${{ runner.temp }}/vt-ios-app/build.xcresult
|
||||
if-no-files-found: error
|
||||
+1
-1
@@ -3412,7 +3412,7 @@ ifeq ($(HAVE_RECORD_MEDIACODEC), 1)
|
||||
DEFINES += -DHAVE_RECORD_MEDIACODEC
|
||||
MEDIA_RECORD_OBJ := record/record_mediacodec_encoder.o \
|
||||
record/record_audio_tap.o \
|
||||
record/record_mux_mediacodec.o record/record_mediacodec_worker.o \
|
||||
record/record_mux_mediacodec.o record/record_mediacodec_worker.o record/record_aac_stream_timing.o \
|
||||
record/record_aac_probe.o \
|
||||
record/record_mp4_audio_edit.o record/record_mediacodec_file_edit.o \
|
||||
record/record_mediacodec_surface.o record/record_mediacodec_file.o \
|
||||
|
||||
@@ -9,8 +9,6 @@
|
||||
#else
|
||||
#define VT_ENCODE_AVAILABLE
|
||||
#endif
|
||||
#include "videotoolbox_encode_hardware.h"
|
||||
|
||||
typedef struct videotoolbox_encode_config
|
||||
{
|
||||
unsigned width, height, slots, bitrate, key_interval, timescale, bit_depth;
|
||||
@@ -59,15 +57,7 @@ static INLINE VT_ENCODE_AVAILABLE videotoolbox_encode_t *videotoolbox_encode_new
|
||||
videotoolbox_encode_t *encoder = NULL;
|
||||
CFMutableDictionaryRef spec = NULL, attrs = NULL, props = NULL;
|
||||
CFTypeRef hardware = NULL, selected_id = NULL;
|
||||
CFStringRef admitted_id = NULL;
|
||||
#ifdef VT_ENCODE_MOBILE
|
||||
bool query_hardware = false;
|
||||
#endif
|
||||
OSStatus status = kVTParameterErr;
|
||||
#ifdef VT_ENCODE_MOBILE
|
||||
if (VT_ENCODE_HARDWARE_KEYS_AVAILABLE)
|
||||
query_hardware = true;
|
||||
#endif
|
||||
if (error) *error = status;
|
||||
if ((encoder_id && !CFStringGetLength(encoder_id)) || !config || !config->width || !config->height || (config->width & 1)
|
||||
|| (config->height & 1) || config->width > 16384 || config->height > 16384
|
||||
@@ -78,20 +68,6 @@ static INLINE VT_ENCODE_AVAILABLE videotoolbox_encode_t *videotoolbox_encode_new
|
||||
|| (config->bit_depth != 8 && config->bit_depth != 10)
|
||||
|| (config->bit_depth == 10 && !config->hevc)
|
||||
|| (config->hdr && config->bit_depth != 10)) return NULL;
|
||||
#ifdef VT_ENCODE_MOBILE
|
||||
if (!query_hardware)
|
||||
{
|
||||
admitted_id = videotoolbox_encode_hardware_id(encoder_id,
|
||||
config->hevc ? kCMVideoCodecType_HEVC : kCMVideoCodecType_H264);
|
||||
if (!admitted_id)
|
||||
{
|
||||
if (error)
|
||||
*error = kVTCouldNotFindVideoEncoderErr;
|
||||
return NULL;
|
||||
}
|
||||
encoder_id = admitted_id;
|
||||
}
|
||||
#endif
|
||||
encoder = (videotoolbox_encode_t*)calloc(1, sizeof(*encoder));
|
||||
spec = CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
|
||||
attrs = CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
|
||||
@@ -100,10 +76,7 @@ static INLINE VT_ENCODE_AVAILABLE videotoolbox_encode_t *videotoolbox_encode_new
|
||||
encoder->config = *config;
|
||||
encoder->pool = videotoolbox_encode_slots_new(config->slots);
|
||||
if (!encoder->pool) { status = kVTAllocationFailedErr; goto done; }
|
||||
#ifdef VT_ENCODE_HAS_HARDWARE_KEYS
|
||||
if (VT_ENCODE_HARDWARE_KEYS_AVAILABLE)
|
||||
CFDictionarySetValue(spec, kVTVideoEncoderSpecification_RequireHardwareAcceleratedVideoEncoder, kCFBooleanTrue);
|
||||
#endif
|
||||
CFDictionarySetValue(spec, kVTVideoEncoderSpecification_RequireHardwareAcceleratedVideoEncoder, kCFBooleanTrue);
|
||||
if (encoder_id) CFDictionarySetValue(spec,kVTVideoEncoderSpecification_EncoderID,encoder_id);
|
||||
if (gpu_registry_id)
|
||||
{
|
||||
@@ -159,15 +132,10 @@ static INLINE VT_ENCODE_AVAILABLE videotoolbox_encode_t *videotoolbox_encode_new
|
||||
if (status) goto done;
|
||||
status = VTCompressionSessionPrepareToEncodeFrames(encoder->session);
|
||||
if (status) goto done;
|
||||
#ifdef VT_ENCODE_HAS_HARDWARE_KEYS
|
||||
if (VT_ENCODE_HARDWARE_KEYS_AVAILABLE)
|
||||
{
|
||||
status = VTSessionCopyProperty(encoder->session,
|
||||
kVTCompressionPropertyKey_UsingHardwareAcceleratedVideoEncoder, NULL, &hardware);
|
||||
if (!status && (!hardware || CFGetTypeID(hardware) != CFBooleanGetTypeID()
|
||||
|| !CFBooleanGetValue((CFBooleanRef)hardware))) status = kVTCouldNotFindVideoEncoderErr;
|
||||
}
|
||||
#endif
|
||||
status = VTSessionCopyProperty(encoder->session,
|
||||
kVTCompressionPropertyKey_UsingHardwareAcceleratedVideoEncoder, NULL, &hardware);
|
||||
if (!status && (!hardware || CFGetTypeID(hardware) != CFBooleanGetTypeID()
|
||||
|| !CFBooleanGetValue((CFBooleanRef)hardware))) status = kVTCouldNotFindVideoEncoderErr;
|
||||
if (!status && encoder_id)
|
||||
{
|
||||
status=VTSessionCopyProperty(encoder->session,kVTCompressionPropertyKey_EncoderID,NULL,&selected_id);
|
||||
@@ -175,7 +143,6 @@ static INLINE VT_ENCODE_AVAILABLE videotoolbox_encode_t *videotoolbox_encode_new
|
||||
|| !CFEqual(selected_id,encoder_id))) status=kVTCouldNotFindVideoEncoderErr;
|
||||
}
|
||||
done:
|
||||
if (admitted_id) CFRelease((CFTypeRef)admitted_id);
|
||||
if (selected_id) CFRelease(selected_id);
|
||||
if (hardware) CFRelease(hardware);
|
||||
if (props) CFRelease(props);
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier: GPL-3.0-or-later */
|
||||
#ifndef RETROARCH_VIDEOTOOLBOX_ENCODE_HARDWARE_H
|
||||
#define RETROARCH_VIDEOTOOLBOX_ENCODE_HARDWARE_H
|
||||
|
||||
#if defined(__APPLE__) && !defined(VIDEOTOOLBOX_ENCODE_TEST_TYPES)
|
||||
#include <TargetConditionals.h>
|
||||
#if TARGET_OS_IPHONE
|
||||
#define VT_ENCODE_MOBILE
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if !defined(VT_ENCODE_MOBILE) \
|
||||
|| (defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 170400) \
|
||||
|| (defined(__TV_OS_VERSION_MAX_ALLOWED) && __TV_OS_VERSION_MAX_ALLOWED >= 170400) \
|
||||
|| (defined(__VISION_OS_VERSION_MAX_ALLOWED) && __VISION_OS_VERSION_MAX_ALLOWED >= 10100)
|
||||
#define VT_ENCODE_HAS_HARDWARE_KEYS
|
||||
#endif
|
||||
|
||||
#if defined(VT_ENCODE_MOBILE) && defined(VT_ENCODE_HAS_HARDWARE_KEYS)
|
||||
#define VT_ENCODE_HARDWARE_KEYS_AVAILABLE \
|
||||
__builtin_available(iOS 17.4, tvOS 17.4, macCatalyst 17.4, visionOS 1.1, *)
|
||||
#elif defined(VT_ENCODE_HAS_HARDWARE_KEYS)
|
||||
#define VT_ENCODE_HARDWARE_KEYS_AVAILABLE true
|
||||
#else
|
||||
#define VT_ENCODE_HARDWARE_KEYS_AVAILABLE false
|
||||
#endif
|
||||
|
||||
#if defined(VT_ENCODE_MOBILE) || defined(VIDEOTOOLBOX_ENCODE_HARDWARE_LIST_TEST)
|
||||
/* Cold admission only. Return a retained, advertised hardware encoder ID;
|
||||
* the caller must independently confirm the session selected that same ID. */
|
||||
static INLINE CFStringRef videotoolbox_encode_hardware_id(
|
||||
CFStringRef requested, uint32_t codec_type)
|
||||
{
|
||||
#if defined(VIDEOTOOLBOX_ENCODE_HARDWARE_LIST_TEST) \
|
||||
|| (defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000) \
|
||||
|| (defined(__TV_OS_VERSION_MAX_ALLOWED) && __TV_OS_VERSION_MAX_ALLOWED >= 130000) \
|
||||
|| defined(__VISION_OS_VERSION_MAX_ALLOWED)
|
||||
#ifndef VIDEOTOOLBOX_ENCODE_HARDWARE_LIST_TEST
|
||||
if (__builtin_available(iOS 13.0, tvOS 13.0, *))
|
||||
#endif
|
||||
{
|
||||
CFArrayRef list = NULL;
|
||||
CFStringRef selected = NULL;
|
||||
CFIndex i, count;
|
||||
OSStatus status = VTCopyVideoEncoderList(NULL, &list);
|
||||
if (status || !list)
|
||||
{
|
||||
if (list)
|
||||
CFRelease(list);
|
||||
return NULL;
|
||||
}
|
||||
count = CFArrayGetCount(list);
|
||||
if (count >= 0 && count <= 4096)
|
||||
for (i = 0; i < count; i++)
|
||||
{
|
||||
CFDictionaryRef entry = (CFDictionaryRef)CFArrayGetValueAtIndex(list, i);
|
||||
CFTypeRef hardware, codec, identifier;
|
||||
int64_t value = 0;
|
||||
if (!entry || CFGetTypeID(entry) != CFDictionaryGetTypeID())
|
||||
continue;
|
||||
hardware = CFDictionaryGetValue(entry, kVTVideoEncoderList_IsHardwareAccelerated);
|
||||
codec = CFDictionaryGetValue(entry, kVTVideoEncoderList_CodecType);
|
||||
identifier = CFDictionaryGetValue(entry, kVTVideoEncoderList_EncoderID);
|
||||
if (!hardware || CFGetTypeID(hardware) != CFBooleanGetTypeID()
|
||||
|| !CFBooleanGetValue((CFBooleanRef)hardware)
|
||||
|| !codec || CFGetTypeID(codec) != CFNumberGetTypeID()
|
||||
|| !CFNumberGetValue((CFNumberRef)codec, kCFNumberSInt64Type, &value)
|
||||
|| value != (int64_t)codec_type
|
||||
|| !identifier || CFGetTypeID(identifier) != CFStringGetTypeID()
|
||||
|| !CFStringGetLength((CFStringRef)identifier)
|
||||
|| (requested && !CFEqual(identifier, requested)))
|
||||
continue;
|
||||
selected = (CFStringRef)CFRetain(identifier);
|
||||
break;
|
||||
}
|
||||
CFRelease(list);
|
||||
return selected;
|
||||
}
|
||||
#endif
|
||||
(void)requested;
|
||||
(void)codec_type;
|
||||
return NULL;
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
@@ -1723,6 +1723,7 @@ RECORDING
|
||||
#include "../record/record_mp4_audio_edit.c"
|
||||
#include "../record/record_mediacodec_encoder.c"
|
||||
#include "../record/record_mux_mediacodec.c"
|
||||
#include "../record/record_aac_stream_timing.c"
|
||||
#include "../record/record_mediacodec_worker.c"
|
||||
#include "../record/record_mediacodec_surface.c"
|
||||
#include "../record/record_mediacodec_file.c"
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
// The cleanroom network stack, as on the other platforms: the crypto,
|
||||
// the encrypted keychain, the TLS 1.2/1.3 client and the SMB2/3 client
|
||||
// with Kerberos, plus the NFSv3/v4 client.
|
||||
NETWORK_STACK_FLAGS = -DHAVE_RTMP -DHAVE_RFLV -DHAVE_CRYPTO -DHAVE_KEYCHAIN -DHAVE_RETROSSL -DHAVE_SMBCLIENT -DHAVE_RETROSMB -DHAVE_NFSCLIENT -DHAVE_RETRONFS
|
||||
NETWORK_STACK_FLAGS = -DHAVE_CRYPTO -DHAVE_KEYCHAIN -DHAVE_RETROSSL -DHAVE_SMBCLIENT -DHAVE_RETROSMB -DHAVE_NFSCLIENT -DHAVE_RETRONFS
|
||||
|
||||
OTHER_CFLAGS = $(inherited) $(NETWORK_STACK_FLAGS)
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
#define RECORD_METAL_H
|
||||
#include "../record_driver.h"
|
||||
#ifdef __APPLE__
|
||||
#include "../record_metal_platform.h"
|
||||
#if defined(RECORD_METAL_NATIVE_SDK) && defined(HAVE_METAL) && defined(HAVE_AVF) \
|
||||
#include <TargetConditionals.h>
|
||||
#if TARGET_OS_OSX && defined(HAVE_METAL) && defined(HAVE_AVF) \
|
||||
&& defined(HAVE_THREADS) && defined(HAVE_CONFIGFILE)
|
||||
#define HAVE_RECORD_METAL
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier: GPL-3.0-or-later */
|
||||
#include "record_aac_stream_timing.h"
|
||||
#include <string.h>
|
||||
bool record_aac_stream_timing(unsigned rate, unsigned measured_delay,
|
||||
record_aac_stream_timing_t *timing)
|
||||
{
|
||||
unsigned decoded_epoch;
|
||||
if (!timing)
|
||||
return false;
|
||||
memset(timing, 0, sizeof(*timing));
|
||||
if (!rate || rate > 192000 || measured_delay > 4096)
|
||||
return false;
|
||||
decoded_epoch = RECORD_AAC_PROBE_FRAMES + RECORD_AAC_STREAM_GUARD
|
||||
+ measured_delay + RECORD_AAC_STREAM_PREROLL;
|
||||
decoded_epoch = (decoded_epoch + 1023) / 1024 * 1024;
|
||||
timing->input_epoch = decoded_epoch - measured_delay;
|
||||
timing->coded_origin = decoded_epoch - RECORD_AAC_STREAM_PREROLL;
|
||||
timing->audio_epoch = RECORD_AAC_STREAM_PREROLL;
|
||||
timing->video_epoch_us = (unsigned)(((uint64_t)timing->audio_epoch
|
||||
* 1000000 + rate - 1) / rate);
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier: GPL-3.0-or-later */
|
||||
#ifndef RECORD_AAC_STREAM_TIMING_H
|
||||
#define RECORD_AAC_STREAM_TIMING_H
|
||||
#include "record_aac_probe.h"
|
||||
RETRO_BEGIN_DECLS
|
||||
#define RECORD_AAC_STREAM_GUARD 4096u
|
||||
#define RECORD_AAC_STREAM_PREROLL 2048u
|
||||
typedef struct record_aac_stream_timing
|
||||
{
|
||||
unsigned input_epoch, coded_origin, audio_epoch, video_epoch_us;
|
||||
} record_aac_stream_timing_t;
|
||||
/* Cold worker policy after runtime calibration, not an encoder-delay guess.
|
||||
* Feed the probe then GUARD silent input frames before awaiting calibration.
|
||||
* Continue silence to input_epoch before accepting original source PCM.
|
||||
* Discard complete coded AAC packets before coded_origin. Transmit the
|
||||
* remaining silent preroll and offset video by video_epoch_us. Keep native
|
||||
* AAC dependencies through this preroll; no MP4 edit or source truncation.
|
||||
* Caller must independently validate native encoded/decoded continuity. */
|
||||
bool record_aac_stream_timing(unsigned rate, unsigned measured_delay,
|
||||
record_aac_stream_timing_t *timing);
|
||||
RETRO_END_DECLS
|
||||
#endif
|
||||
@@ -15,12 +15,11 @@
|
||||
#undef RECORD_METAL_DISPATCH_UNDEF_INLINE
|
||||
#endif
|
||||
#include "record_gpu_dispatch_metal.h"
|
||||
#include "record_metal_platform.h"
|
||||
#include <retro_atomic.h>
|
||||
#include <defines/cocoa_defines.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#if defined(RECORD_METAL_NATIVE_SDK) && defined(HAVE_THREADS) && defined(RETRO_ATOMIC_LOCK_FREE)
|
||||
#if TARGET_OS_OSX && defined(HAVE_THREADS) && defined(RETRO_ATOMIC_LOCK_FREE)
|
||||
#include <pthread.h>
|
||||
#if !defined(__cplusplus) && (!defined(__STDC_VERSION__) || __STDC_VERSION__<199901L) && !defined(inline)
|
||||
#define inline __inline__
|
||||
@@ -448,11 +447,6 @@ static record_gpu_dispatch_metal_t *record_metal_dispatch_new(void *device,
|
||||
id<MTLDevice> metal=(__bridge id<MTLDevice>)device;
|
||||
size_t length;
|
||||
int error;
|
||||
if (RECORD_METAL_NATIVE_RUNTIME)
|
||||
{
|
||||
}
|
||||
else
|
||||
return NULL;
|
||||
if (!metal || !config || (!output && !stream)
|
||||
|| (output && output->video_format) || !path || !*path) return NULL;
|
||||
#ifndef RECORD_METAL_STREAM
|
||||
|
||||
@@ -17,22 +17,16 @@
|
||||
#endif
|
||||
#include <string.h>
|
||||
#include "record_gpu_session_metal.h"
|
||||
#include "record_metal_platform.h"
|
||||
#include "record_avfoundation_session_native.h"
|
||||
#if defined(VIDEOTOOLBOX_ENCODE_SLOTS) && defined(RECORD_METAL_NATIVE_SDK)
|
||||
#if defined(VIDEOTOOLBOX_ENCODE_SLOTS) && TARGET_OS_OSX \
|
||||
&& defined(__MAC_OS_X_VERSION_MAX_ALLOWED) && __MAC_OS_X_VERSION_MAX_ALLOWED>=101400
|
||||
/* Enumerated IDs prevent silent default/software/other-GPU selection. A
|
||||
* hardware encoder with no advertised GPU is a separate explicit category,
|
||||
* as used by the hosted paravirtualized media engines. It is not evidence of
|
||||
* physical same-GPU encoding; CVPixelBuffer/Metal interop remains gated. */
|
||||
static RECORD_METAL_NATIVE_AVAILABLE videotoolbox_encode_t *record_metal_select_encoder(
|
||||
static __attribute__((availability(macos,introduced=10.14))) videotoolbox_encode_t *record_metal_select_encoder(
|
||||
const videotoolbox_encode_config_t *config,uint64_t renderer_gpu,OSStatus *error)
|
||||
{
|
||||
#if TARGET_OS_IPHONE
|
||||
/* Mobile admission verifies hardware and the selected ID in the VT adapter.
|
||||
* CVPixelBuffer/Metal compatibility is still checked by capture creation. */
|
||||
(void)renderer_gpu;
|
||||
return videotoolbox_encode_new(config, error);
|
||||
#else
|
||||
CFArrayRef list=NULL;
|
||||
CFIndex i,count;
|
||||
unsigned priority;
|
||||
@@ -68,7 +62,6 @@ static RECORD_METAL_NATIVE_AVAILABLE videotoolbox_encode_t *record_metal_select_
|
||||
}
|
||||
done:
|
||||
CFRelease(list); *error=status; return encoder;
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
record_avfoundation_session_t *record_gpu_session_metal_new_output(void *device,
|
||||
@@ -80,8 +73,8 @@ record_avfoundation_session_t *record_gpu_session_metal_new_output(void *device,
|
||||
if (queue_out) *queue_out=NULL;
|
||||
if (stage) *stage=RECORD_METAL_STARTUP_UNSUPPORTED;
|
||||
if (native_status) *native_status=0;
|
||||
#if defined(VIDEOTOOLBOX_ENCODE_SLOTS) && defined(RECORD_METAL_NATIVE_SDK)
|
||||
if (RECORD_METAL_NATIVE_RUNTIME)
|
||||
#if defined(VIDEOTOOLBOX_ENCODE_SLOTS) && TARGET_OS_OSX \
|
||||
&& defined(__MAC_OS_X_VERSION_MAX_ALLOWED) && __MAC_OS_X_VERSION_MAX_ALLOWED>=101400
|
||||
@autoreleasepool
|
||||
{
|
||||
videotoolbox_encode_config_t config;
|
||||
@@ -101,12 +94,8 @@ record_avfoundation_session_t *record_gpu_session_metal_new_output(void *device,
|
||||
|| !c->width || !c->height || c->width>16384 || c->height>16384
|
||||
|| ((c->width|c->height)&1) || c->slots<2 || c->slots>VIDEOTOOLBOX_ENCODE_SLOTS
|
||||
|| c->mode>2 || (c->mode && c->bit_depth!=10)) goto done;
|
||||
#if TARGET_OS_IPHONE
|
||||
gpu = 0;
|
||||
#else
|
||||
gpu=[(__bridge id<MTLDevice>)device registryID];
|
||||
if (!gpu) goto done;
|
||||
#endif
|
||||
config.width=c->width; config.height=c->height; config.slots=c->slots;
|
||||
config.bitrate=c->bitrate; config.key_interval=c->key_interval; config.timescale=c->timescale;
|
||||
config.bit_depth=c->bit_depth; config.hevc=c->hevc; config.hdr=c->mode!=0;
|
||||
@@ -139,20 +128,20 @@ done:
|
||||
if (native_status) *native_status=(int32_t)status;
|
||||
return session;
|
||||
}
|
||||
return NULL;
|
||||
#else
|
||||
(void)device; (void)c; (void)bind; (void)context;
|
||||
return NULL;
|
||||
#endif
|
||||
}
|
||||
#if defined(VIDEOTOOLBOX_ENCODE_SLOTS) && defined(RECORD_METAL_NATIVE_SDK)
|
||||
#if defined(VIDEOTOOLBOX_ENCODE_SLOTS) && TARGET_OS_OSX \
|
||||
&& defined(__MAC_OS_X_VERSION_MAX_ALLOWED) && __MAC_OS_X_VERSION_MAX_ALLOWED>=101400
|
||||
typedef struct record_metal_file_binding
|
||||
{
|
||||
const record_mux_avfoundation_config_t *output;
|
||||
const char *path;
|
||||
size_t audio_capacity;
|
||||
} record_metal_file_binding_t;
|
||||
static RECORD_METAL_NATIVE_AVAILABLE record_avfoundation_session_t *record_metal_file_bind(
|
||||
static __attribute__((availability(macos,introduced=10.14))) record_avfoundation_session_t *record_metal_file_bind(
|
||||
void *data,record_gpu_queue_t *queue,record_gpu_capture_metal_t *capture,
|
||||
struct videotoolbox_encode *encoder,unsigned width,unsigned height)
|
||||
{
|
||||
@@ -170,8 +159,8 @@ record_avfoundation_session_t *record_gpu_session_metal_new(void *device,
|
||||
if (queue_out) *queue_out=NULL;
|
||||
if (stage) *stage=RECORD_METAL_STARTUP_UNSUPPORTED;
|
||||
if (native_status) *native_status=0;
|
||||
#if defined(VIDEOTOOLBOX_ENCODE_SLOTS) && defined(RECORD_METAL_NATIVE_SDK)
|
||||
if (RECORD_METAL_NATIVE_RUNTIME)
|
||||
#if defined(VIDEOTOOLBOX_ENCODE_SLOTS) && TARGET_OS_OSX \
|
||||
&& defined(__MAC_OS_X_VERSION_MAX_ALLOWED) && __MAC_OS_X_VERSION_MAX_ALLOWED>=101400
|
||||
{
|
||||
record_metal_file_binding_t binding;
|
||||
if (!c || !output || output->video_format || c->timescale!=output->timescale || !path || !*path)
|
||||
@@ -183,7 +172,6 @@ record_avfoundation_session_t *record_gpu_session_metal_new(void *device,
|
||||
return record_gpu_session_metal_new_output(device,c,record_metal_file_bind,&binding,
|
||||
capture_out,queue_out,stage,native_status);
|
||||
}
|
||||
return NULL;
|
||||
#else
|
||||
(void)device; (void)c; (void)output; (void)audio_capacity_bytes; (void)path;
|
||||
return NULL;
|
||||
|
||||
@@ -466,6 +466,24 @@ void *record_mediacodec_encoder_format(record_mediacodec_encoder_t *s)
|
||||
{
|
||||
return s && !s->failed ? s->format : NULL;
|
||||
}
|
||||
bool record_mediacodec_encoder_video_config(record_mediacodec_encoder_t *s,
|
||||
unsigned index, const void **data, size_t *bytes)
|
||||
{
|
||||
static const char *const keys[2] = {"csd-0", "csd-1"};
|
||||
void *buffer = NULL;
|
||||
size_t size = 0;
|
||||
if (data)
|
||||
*data = NULL;
|
||||
if (bytes)
|
||||
*bytes = 0;
|
||||
if (!s || !data || !bytes || s->failed || s->audio || !s->format
|
||||
|| index >= 2 || !s->get_buffer(s->format, keys[index], &buffer, &size)
|
||||
|| !buffer || !size || size > 65535)
|
||||
return false;
|
||||
*data = buffer;
|
||||
*bytes = size;
|
||||
return true;
|
||||
}
|
||||
bool record_mediacodec_encoder_audio_config(record_mediacodec_encoder_t *s,
|
||||
const void **data, size_t *bytes)
|
||||
{
|
||||
@@ -666,6 +684,17 @@ void *record_mediacodec_encoder_format(record_mediacodec_encoder_t *s)
|
||||
(void)s;
|
||||
return NULL;
|
||||
}
|
||||
bool record_mediacodec_encoder_video_config(record_mediacodec_encoder_t *s,
|
||||
unsigned index, const void **data, size_t *bytes)
|
||||
{
|
||||
(void)s;
|
||||
(void)index;
|
||||
if (data)
|
||||
*data = NULL;
|
||||
if (bytes)
|
||||
*bytes = 0;
|
||||
return false;
|
||||
}
|
||||
bool record_mediacodec_encoder_audio_config(record_mediacodec_encoder_t *s,
|
||||
const void **data, size_t *bytes)
|
||||
{
|
||||
|
||||
@@ -51,6 +51,14 @@ void *record_mediacodec_encoder_surface(record_mediacodec_encoder_t *encoder);
|
||||
int record_mediacodec_encoder_pump(
|
||||
record_mediacodec_encoder_t *encoder, record_mediacodec_packet_t *packet);
|
||||
void *record_mediacodec_encoder_format(record_mediacodec_encoder_t *encoder);
|
||||
/* Worker-only video CSD buffer 0 or 1, at most 65535 bytes each.
|
||||
* Native encoding is unchanged (AVC SPS/PPS are separate buffers; HEVC
|
||||
* parameter sets share buffer 0). Borrowed until the next pump, stop or free.
|
||||
* No allocation, packet dequeue/release or payload conversion. Missing or
|
||||
* invalid metadata clears both outputs without poisoning the encoder. */
|
||||
bool record_mediacodec_encoder_video_config(
|
||||
record_mediacodec_encoder_t *encoder, unsigned index,
|
||||
const void **data, size_t *bytes);
|
||||
/* AAC configuration borrowed for the current output format. */
|
||||
bool record_mediacodec_encoder_audio_config(
|
||||
record_mediacodec_encoder_t *encoder, const void **data, size_t *bytes);
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier: GPL-3.0-or-later */
|
||||
#include "record_mediacodec_stream_config.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
rflv_mux_t *record_mediacodec_stream_mux_new(record_mediacodec_encoder_t *video,
|
||||
record_mediacodec_encoder_t *audio, const record_mediacodec_stream_config_t *config)
|
||||
{
|
||||
rflv_config_t mux_config;
|
||||
rflv_mux_t *mux;
|
||||
const void *first = NULL, *second = NULL;
|
||||
size_t first_size = 0, second_size = 0;
|
||||
uint8_t *joined = NULL;
|
||||
if (!video || !config || (unsigned)config->codec > RECORD_MC_STREAM_AV1
|
||||
|| (config->bit_depth != 8 && config->bit_depth != 10)
|
||||
|| (config->codec == RECORD_MC_STREAM_AVC && config->bit_depth != 8)
|
||||
|| !config->max_packet_size
|
||||
|| config->max_packet_size > (config->codec == RECORD_MC_STREAM_HEVC
|
||||
? 0xfffff7u : 0xfffffau))
|
||||
return NULL;
|
||||
memset(&mux_config, 0, sizeof(mux_config));
|
||||
if (!record_mediacodec_encoder_video_config(video, 0, &first, &first_size))
|
||||
return NULL;
|
||||
mux_config.avcc = first;
|
||||
mux_config.avcc_size = first_size;
|
||||
if (config->codec == RECORD_MC_STREAM_AVC
|
||||
&& record_mediacodec_encoder_video_config(video, 1, &second, &second_size))
|
||||
{
|
||||
if (!first || !second || !first_size || !second_size
|
||||
|| first_size > 65535 || second_size > 65535)
|
||||
return NULL;
|
||||
joined = (uint8_t*)malloc(first_size + second_size);
|
||||
if (!joined)
|
||||
return NULL;
|
||||
memcpy(joined, first, first_size);
|
||||
memcpy(joined + first_size, second, second_size);
|
||||
mux_config.avcc = joined;
|
||||
mux_config.avcc_size = first_size + second_size;
|
||||
}
|
||||
mux_config.video_timescale = 1000000;
|
||||
mux_config.max_packet_size = config->max_packet_size;
|
||||
if (audio)
|
||||
{
|
||||
if (!record_mediacodec_encoder_audio_config(audio, &mux_config.asc, &mux_config.asc_size))
|
||||
{
|
||||
free(joined);
|
||||
return NULL;
|
||||
}
|
||||
mux_config.audio_timescale = 1000000;
|
||||
}
|
||||
if (config->codec == RECORD_MC_STREAM_AVC)
|
||||
mux = rflv_mux_new_annexb(&mux_config);
|
||||
else if (config->codec == RECORD_MC_STREAM_HEVC)
|
||||
mux = rflv_mux_new_hevc_annexb(&mux_config);
|
||||
else
|
||||
mux = rflv_mux_new_av1(&mux_config);
|
||||
free(joined);
|
||||
if (mux && config->codec != RECORD_MC_STREAM_AVC
|
||||
&& !rflv_mux_set_color(mux, &config->color, config->bit_depth))
|
||||
{
|
||||
rflv_mux_free(mux);
|
||||
return NULL;
|
||||
}
|
||||
return mux;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier: GPL-3.0-or-later */
|
||||
#ifndef RECORD_MEDIACODEC_STREAM_CONFIG_H
|
||||
#define RECORD_MEDIACODEC_STREAM_CONFIG_H
|
||||
#include "record_mediacodec_encoder.h"
|
||||
#include <formats/rflv_mux.h>
|
||||
RETRO_BEGIN_DECLS
|
||||
|
||||
enum record_mediacodec_stream_codec
|
||||
{
|
||||
RECORD_MC_STREAM_AVC, RECORD_MC_STREAM_HEVC, RECORD_MC_STREAM_AV1
|
||||
};
|
||||
typedef struct record_mediacodec_stream_config
|
||||
{
|
||||
video_color_metadata_t color;
|
||||
size_t max_packet_size;
|
||||
enum record_mediacodec_stream_codec codec;
|
||||
unsigned bit_depth;
|
||||
} record_mediacodec_stream_config_t;
|
||||
/* Cold I/O worker only, after all enabled encoders report FORMAT. Copies setup
|
||||
* data into the shared mux; retains neither encoder nor native format/CSD.
|
||||
* AVC/HEVC use push_annexb; AV1 uses ordinary push with sized OBUs. AAC is raw.
|
||||
* Both tracks use MediaCodec packet pts_us (timescale 1000000); no rebasing.
|
||||
* Caller owns the fresh mux. Packet/output lifetimes are a separate contract. */
|
||||
rflv_mux_t *record_mediacodec_stream_mux_new(record_mediacodec_encoder_t *video,
|
||||
record_mediacodec_encoder_t *audio, const record_mediacodec_stream_config_t *config);
|
||||
RETRO_END_DECLS
|
||||
#endif
|
||||
@@ -1,6 +1,7 @@
|
||||
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier:
|
||||
* GPL-3.0-or-later */
|
||||
#include "record_mediacodec_worker.h"
|
||||
#include "record_aac_stream_timing.h"
|
||||
#include <stdlib.h>
|
||||
|
||||
struct record_mediacodec_worker
|
||||
@@ -9,13 +10,17 @@ struct record_mediacodec_worker
|
||||
int64_t end_us[2];
|
||||
record_mediacodec_encoder_t *encoder[2];
|
||||
record_audio_queue_t *pcm;
|
||||
record_mux_mediacodec_t *mux;
|
||||
record_mediacodec_output_ops_t output;
|
||||
void *output_context;
|
||||
record_mediacodec_packet_t pending[2];
|
||||
record_aac_probe_t *probe;
|
||||
record_aac_stream_timing_t live_timing;
|
||||
size_t consumed, prefix_sent;
|
||||
unsigned channels, rate;
|
||||
bool format[2], drained[2], ended[2];
|
||||
bool format[2], drained[2], ended[2], pending_live[2];
|
||||
bool active, closing, video_eos, audio_eos, pcm_end;
|
||||
bool started, finished, failed;
|
||||
bool live_probe, timing_ready;
|
||||
};
|
||||
|
||||
static bool record_mcw_samples_us(uint64_t frames, unsigned rate,
|
||||
@@ -41,13 +46,24 @@ bool record_mediacodec_worker_calibrate(record_mediacodec_worker_t *w,
|
||||
return true;
|
||||
}
|
||||
|
||||
bool record_mediacodec_worker_calibrate_live(record_mediacodec_worker_t *w,
|
||||
record_aac_probe_t *probe)
|
||||
{
|
||||
record_aac_stream_timing_t timing;
|
||||
if (!w || !record_aac_stream_timing(w->rate, 0, &timing)
|
||||
|| !record_mediacodec_worker_calibrate(w, probe))
|
||||
return false;
|
||||
w->live_probe = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool record_mediacodec_worker_audio_edit(
|
||||
const record_mediacodec_worker_t *w, uint64_t *skip,
|
||||
uint64_t *frames, unsigned *roll)
|
||||
{
|
||||
unsigned delay;
|
||||
if (!w || !skip || !frames || !roll || !w->finished || w->failed
|
||||
|| !w->probe || !record_aac_probe_delay(w->probe, &delay))
|
||||
|| !w->probe || w->live_probe || !record_aac_probe_delay(w->probe, &delay))
|
||||
return false;
|
||||
*skip = RECORD_AAC_PROBE_FRAMES + (uint64_t)delay;
|
||||
*frames = w->audio_end_sample;
|
||||
@@ -55,13 +71,40 @@ bool record_mediacodec_worker_audio_edit(
|
||||
return true;
|
||||
}
|
||||
|
||||
record_mediacodec_worker_t *record_mediacodec_worker_new(
|
||||
static bool record_mcw_file_track(void *context, bool audio, void *format)
|
||||
{
|
||||
return record_mux_mediacodec_track((record_mux_mediacodec_t*)context, audio, format);
|
||||
}
|
||||
static int record_mcw_file_start(void *context)
|
||||
{
|
||||
return record_mux_mediacodec_start((record_mux_mediacodec_t*)context);
|
||||
}
|
||||
static int record_mcw_file_write(void *context, bool audio,
|
||||
const record_mediacodec_packet_t *packet)
|
||||
{
|
||||
return record_mux_mediacodec_write((record_mux_mediacodec_t*)context, audio, packet) ? 1 : -1;
|
||||
}
|
||||
static bool record_mcw_file_end(void *context, bool audio, int64_t end_us)
|
||||
{
|
||||
return record_mux_mediacodec_end((record_mux_mediacodec_t*)context, audio, end_us);
|
||||
}
|
||||
static int record_mcw_file_finish(void *context)
|
||||
{
|
||||
return record_mux_mediacodec_finish((record_mux_mediacodec_t*)context);
|
||||
}
|
||||
static bool record_mcw_file_retire(void *context)
|
||||
{
|
||||
(void)context;
|
||||
return true;
|
||||
}
|
||||
record_mediacodec_worker_t *record_mediacodec_worker_new_output(
|
||||
record_mediacodec_encoder_t *video, record_mediacodec_encoder_t *audio,
|
||||
record_audio_queue_t *pcm, record_mux_mediacodec_t *mux,
|
||||
unsigned channels, unsigned rate)
|
||||
record_audio_queue_t *pcm, const record_mediacodec_output_ops_t *ops,
|
||||
void *context, unsigned channels, unsigned rate)
|
||||
{
|
||||
record_mediacodec_worker_t *w;
|
||||
if (!video || !mux || (!!audio != !!pcm)
|
||||
if (!video || !context || !ops || !ops->track || !ops->start || !ops->write
|
||||
|| !ops->end || !ops->finish || !ops->retire || (!!audio != !!pcm)
|
||||
|| (audio && (!channels || channels > 2 || !rate)))
|
||||
return NULL;
|
||||
w = (record_mediacodec_worker_t *)calloc(1, sizeof(*w));
|
||||
@@ -70,12 +113,25 @@ record_mediacodec_worker_t *record_mediacodec_worker_new(
|
||||
w->encoder[0] = video;
|
||||
w->encoder[1] = audio;
|
||||
w->pcm = pcm;
|
||||
w->mux = mux;
|
||||
w->output = *ops;
|
||||
w->output_context = context;
|
||||
w->channels = channels;
|
||||
w->rate = rate;
|
||||
w->drained[1] = w->ended[1] = !audio;
|
||||
return w;
|
||||
}
|
||||
record_mediacodec_worker_t *record_mediacodec_worker_new(
|
||||
record_mediacodec_encoder_t *video, record_mediacodec_encoder_t *audio,
|
||||
record_audio_queue_t *pcm, record_mux_mediacodec_t *mux,
|
||||
unsigned channels, unsigned rate)
|
||||
{
|
||||
static const record_mediacodec_output_ops_t ops = {
|
||||
record_mcw_file_track, record_mcw_file_start, record_mcw_file_write,
|
||||
record_mcw_file_end, record_mcw_file_finish, record_mcw_file_retire
|
||||
};
|
||||
return record_mediacodec_worker_new_output(video, audio, pcm, &ops,
|
||||
mux, channels, rate);
|
||||
}
|
||||
|
||||
bool record_mediacodec_worker_finish(record_mediacodec_worker_t *w,
|
||||
int64_t video_end_us, uint64_t audio_end_sample)
|
||||
@@ -89,7 +145,7 @@ bool record_mediacodec_worker_finish(record_mediacodec_worker_t *w,
|
||||
{
|
||||
if ((!audio_end_sample && !w->probe)
|
||||
|| (w->probe && audio_end_sample
|
||||
> (uint64_t)-1 - RECORD_AAC_PROBE_FRAMES)
|
||||
> (uint64_t)-1 - (w->live_probe ? 32768u : RECORD_AAC_PROBE_FRAMES))
|
||||
|| !record_mcw_samples_us(audio_end_sample, w->rate,
|
||||
&w->end_us[1]))
|
||||
return false;
|
||||
@@ -106,7 +162,13 @@ static int record_mcw_output(record_mediacodec_worker_t *w, unsigned at)
|
||||
int result;
|
||||
if (w->drained[at])
|
||||
return 0;
|
||||
result = record_mediacodec_encoder_pump(w->encoder[at], &packet);
|
||||
if (w->pending_live[at])
|
||||
{
|
||||
packet = w->pending[at];
|
||||
result = RECORD_MC_PACKET;
|
||||
}
|
||||
else
|
||||
result = record_mediacodec_encoder_pump(w->encoder[at], &packet);
|
||||
switch (result)
|
||||
{
|
||||
case RECORD_MC_BUSY:
|
||||
@@ -123,7 +185,7 @@ static int record_mcw_output(record_mediacodec_worker_t *w, unsigned at)
|
||||
|| !record_aac_probe_config(w->probe, config, bytes))
|
||||
return -1;
|
||||
}
|
||||
if (w->format[at] || !record_mux_mediacodec_track(w->mux, at != 0,
|
||||
if (w->format[at] || !w->output.track(w->output_context, at != 0,
|
||||
record_mediacodec_encoder_format(w->encoder[at])))
|
||||
return -1;
|
||||
w->format[at] = true;
|
||||
@@ -133,8 +195,11 @@ static int record_mcw_output(record_mediacodec_worker_t *w, unsigned at)
|
||||
return -1;
|
||||
if (!w->started)
|
||||
return 0;
|
||||
if (at && w->probe && !(packet.flags & 2))
|
||||
if (!at && w->live_probe && !w->timing_ready)
|
||||
return 0;
|
||||
if (!w->pending_live[at] && at && w->probe && packet.size && !(packet.flags & 2))
|
||||
{
|
||||
uint64_t coded = w->coded_frames;
|
||||
if (w->coded_frames > (uint64_t)-1 - 1024
|
||||
|| !record_mcw_samples_us(w->coded_frames, w->rate,
|
||||
&packet.pts_us)
|
||||
@@ -142,10 +207,36 @@ static int record_mcw_output(record_mediacodec_worker_t *w, unsigned at)
|
||||
packet.size))
|
||||
return -1;
|
||||
w->coded_frames += 1024;
|
||||
if (w->live_probe)
|
||||
{
|
||||
if (!w->timing_ready || coded < w->live_timing.coded_origin)
|
||||
return record_mediacodec_encoder_release(w->encoder[at]) ? 1 : -1;
|
||||
if (!record_mcw_samples_us(coded - w->live_timing.coded_origin,
|
||||
w->rate, &packet.pts_us))
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
if (!record_mux_mediacodec_write(w->mux, at != 0, &packet)
|
||||
|| !record_mediacodec_encoder_release(w->encoder[at]))
|
||||
if (!w->pending_live[at])
|
||||
{
|
||||
if (!at && w->live_probe)
|
||||
{
|
||||
uint64_t maximum = ((uint64_t)1 << 63) - 1;
|
||||
if (packet.pts_us < 0 || (uint64_t)packet.pts_us
|
||||
> maximum - w->live_timing.video_epoch_us)
|
||||
return -1;
|
||||
packet.pts_us += w->live_timing.video_epoch_us;
|
||||
}
|
||||
w->pending[at] = packet;
|
||||
w->pending_live[at] = true;
|
||||
}
|
||||
result = w->output.write(w->output_context, at != 0, &w->pending[at]);
|
||||
if (result < 0)
|
||||
return -1;
|
||||
if (!result)
|
||||
return 0;
|
||||
if (!record_mediacodec_encoder_release(w->encoder[at]))
|
||||
return -1;
|
||||
w->pending_live[at] = false;
|
||||
return 1;
|
||||
case RECORD_MC_END:
|
||||
if (!w->format[at] || !w->closing
|
||||
@@ -160,6 +251,7 @@ static int record_mcw_output(record_mediacodec_worker_t *w, unsigned at)
|
||||
|
||||
static int record_mcw_input(record_mediacodec_worker_t *w)
|
||||
{
|
||||
static const int16_t silence[512 * 2] = {0};
|
||||
const int16_t *samples = NULL;
|
||||
size_t frames = 0, accepted = 0;
|
||||
uint64_t timestamp = 0;
|
||||
@@ -180,6 +272,25 @@ static int record_mcw_input(record_mediacodec_worker_t *w)
|
||||
w->prefix_sent += accepted;
|
||||
return result ? 1 : 0;
|
||||
}
|
||||
if (w->live_probe)
|
||||
{
|
||||
size_t target = w->timing_ready ? w->live_timing.input_epoch
|
||||
: RECORD_AAC_PROBE_FRAMES + RECORD_AAC_STREAM_GUARD;
|
||||
if (w->prefix_sent < target)
|
||||
{
|
||||
frames = target - w->prefix_sent;
|
||||
if (frames > 512)
|
||||
frames = 512;
|
||||
result = record_mediacodec_encoder_audio_submit(w->encoder[1],
|
||||
silence, frames, w->prefix_sent, &accepted);
|
||||
if (result < 0 || result > 1 || accepted > frames || (!result && accepted))
|
||||
return -1;
|
||||
w->prefix_sent += accepted;
|
||||
return result ? 1 : 0;
|
||||
}
|
||||
if (!w->timing_ready)
|
||||
return 0;
|
||||
}
|
||||
if (!w->pcm_end)
|
||||
{
|
||||
result = record_audio_queue_peek(w->pcm, &samples, &frames, ×tamp);
|
||||
@@ -192,13 +303,15 @@ static int record_mcw_input(record_mediacodec_worker_t *w)
|
||||
if (w->consumed >= frames || frames > (size_t)-1 / w->channels
|
||||
|| timestamp > (uint64_t)-1 - frames
|
||||
|| (w->probe && timestamp + frames
|
||||
> (uint64_t)-1 - RECORD_AAC_PROBE_FRAMES)
|
||||
> (uint64_t)-1 - (w->live_probe
|
||||
? w->live_timing.input_epoch : RECORD_AAC_PROBE_FRAMES))
|
||||
|| (w->closing && timestamp + frames > w->audio_end_sample))
|
||||
return -1;
|
||||
result = record_mediacodec_encoder_audio_submit(w->encoder[1],
|
||||
samples + w->consumed * w->channels, frames - w->consumed,
|
||||
timestamp + w->consumed
|
||||
+ (w->probe ? RECORD_AAC_PROBE_FRAMES : 0), &accepted);
|
||||
+ (w->live_probe ? w->live_timing.input_epoch
|
||||
: w->probe ? RECORD_AAC_PROBE_FRAMES : 0), &accepted);
|
||||
if (result < 0 || result > 1 || accepted > frames - w->consumed
|
||||
|| (!result && accepted))
|
||||
return -1;
|
||||
@@ -218,7 +331,8 @@ static int record_mcw_input(record_mediacodec_worker_t *w)
|
||||
return 0;
|
||||
result = record_mediacodec_encoder_audio_finish(
|
||||
w->encoder[1], w->audio_end_sample
|
||||
+ (w->probe ? RECORD_AAC_PROBE_FRAMES : 0));
|
||||
+ (w->live_probe ? w->live_timing.input_epoch
|
||||
: w->probe ? RECORD_AAC_PROBE_FRAMES : 0));
|
||||
if (result < 0 || result > 2)
|
||||
return -1;
|
||||
if (result == 1)
|
||||
@@ -262,23 +376,35 @@ int record_mediacodec_worker_step(record_mediacodec_worker_t *w)
|
||||
goto failed;
|
||||
calibrated = result == 2;
|
||||
progress |= result == 1;
|
||||
if (w->live_probe && calibrated && !w->timing_ready)
|
||||
{
|
||||
unsigned delay;
|
||||
if (!record_aac_probe_delay(w->probe, &delay)
|
||||
|| !record_aac_stream_timing(w->rate, delay, &w->live_timing))
|
||||
goto failed;
|
||||
w->timing_ready = true;
|
||||
progress = true;
|
||||
}
|
||||
if (w->drained[1] && calibrated)
|
||||
{
|
||||
unsigned delay;
|
||||
uint64_t skip;
|
||||
if (!record_aac_probe_delay(w->probe, &delay))
|
||||
goto failed;
|
||||
skip = RECORD_AAC_PROBE_FRAMES + (uint64_t)delay;
|
||||
skip = (w->live_probe ? w->live_timing.input_epoch
|
||||
: RECORD_AAC_PROBE_FRAMES) + (uint64_t)delay;
|
||||
if ((w->audio_end_sample && (w->coded_frames < skip
|
||||
|| w->coded_frames - skip < w->audio_end_sample))
|
||||
|| !record_mcw_samples_us(w->coded_frames, w->rate,
|
||||
|| !record_mcw_samples_us(w->live_probe
|
||||
? w->live_timing.audio_epoch + w->audio_end_sample
|
||||
: w->coded_frames, w->rate,
|
||||
&w->end_us[1]))
|
||||
goto failed;
|
||||
}
|
||||
}
|
||||
if (!w->started)
|
||||
{
|
||||
result = record_mux_mediacodec_start(w->mux);
|
||||
result = w->output.start(w->output_context);
|
||||
if (result < 0)
|
||||
goto failed;
|
||||
if (result)
|
||||
@@ -291,14 +417,24 @@ int record_mediacodec_worker_step(record_mediacodec_worker_t *w)
|
||||
if (w->started && w->drained[at] && !w->ended[at]
|
||||
&& (!at || calibrated))
|
||||
{
|
||||
if (!record_mux_mediacodec_end(w->mux, at != 0, w->end_us[at]))
|
||||
int64_t end = w->end_us[at];
|
||||
if (!at && w->live_probe)
|
||||
{
|
||||
uint64_t maximum = ((uint64_t)1 << 63) - 1;
|
||||
if (!w->timing_ready)
|
||||
continue;
|
||||
if ((uint64_t)end > maximum - w->live_timing.video_epoch_us)
|
||||
goto failed;
|
||||
end += w->live_timing.video_epoch_us;
|
||||
}
|
||||
if (!w->output.end(w->output_context, at != 0, end))
|
||||
goto failed;
|
||||
w->ended[at] = true;
|
||||
progress = true;
|
||||
}
|
||||
if (w->ended[0] && w->ended[1])
|
||||
{
|
||||
result = record_mux_mediacodec_finish(w->mux);
|
||||
result = w->output.finish(w->output_context);
|
||||
if (result < 0)
|
||||
goto failed;
|
||||
if (result)
|
||||
@@ -323,6 +459,8 @@ bool record_mediacodec_worker_free(record_mediacodec_worker_t *w)
|
||||
{
|
||||
if (w && w->active && !w->failed && !w->finished)
|
||||
return false;
|
||||
if (w && !w->output.retire(w->output_context))
|
||||
return false;
|
||||
free(w);
|
||||
return true;
|
||||
}
|
||||
@@ -14,6 +14,28 @@ enum record_mediacodec_worker_result
|
||||
RECORD_MC_WORKER_PROGRESS,
|
||||
RECORD_MC_WORKER_END
|
||||
};
|
||||
typedef struct record_mediacodec_output_ops
|
||||
{
|
||||
/* Format is borrowed only during this call; copy setup metadata here. */
|
||||
bool (*track)(void *context, bool audio, void *format);
|
||||
/* start/finish: -1 error, 0 BUSY, 1 complete. */
|
||||
int (*start)(void *context);
|
||||
/* -1 error, 0 BUSY, 1 accepted. BUSY retains the same immutable packet
|
||||
* descriptor and native bytes; accepted means all output borrows retired. */
|
||||
int (*write)(void *context, bool audio, const record_mediacodec_packet_t *packet);
|
||||
bool (*end)(void *context, bool audio, int64_t end_us);
|
||||
int (*finish)(void *context);
|
||||
/* Bounded worker-only cancellation/retirement. false requires a later
|
||||
* free retry; true guarantees no remaining packet/format borrow. */
|
||||
bool (*retire)(void *context);
|
||||
} record_mediacodec_output_ops_t;
|
||||
/* Copies the callback table; borrows context and encoders until free succeeds.
|
||||
* Output owns no encoder: retire must quiesce all its borrows before caller
|
||||
* stops or destroys encoders. Audio encoder and PCM queue are paired. */
|
||||
record_mediacodec_worker_t *record_mediacodec_worker_new_output(
|
||||
record_mediacodec_encoder_t *video, record_mediacodec_encoder_t *audio,
|
||||
record_audio_queue_t *pcm, const record_mediacodec_output_ops_t *ops,
|
||||
void *context, unsigned channels, unsigned rate);
|
||||
/* One I/O worker borrows all components. Audio encoder and queue must both
|
||||
* be present or absent. channels/rate match their cold configuration. */
|
||||
record_mediacodec_worker_t *record_mediacodec_worker_new(
|
||||
@@ -23,6 +45,12 @@ record_mediacodec_worker_t *record_mediacodec_worker_new(
|
||||
/* Before service starts; caller retains the probe until worker retirement. */
|
||||
bool record_mediacodec_worker_calibrate(record_mediacodec_worker_t *worker,
|
||||
record_aac_probe_t *probe);
|
||||
/* Live output only, before service. Uses measured delay and silent preroll;
|
||||
* calibration packets never reach output. Original PCM timestamps stay in
|
||||
* the source epoch; output video is shifted to match the audible source.
|
||||
* Probe and encoders remain borrowed through worker retirement. */
|
||||
bool record_mediacodec_worker_calibrate_live(record_mediacodec_worker_t *worker,
|
||||
record_aac_probe_t *probe);
|
||||
/* Completed calibrated recording only; source frames exclude the prefix. */
|
||||
bool record_mediacodec_worker_audio_edit(
|
||||
const record_mediacodec_worker_t *worker, uint64_t *skip,
|
||||
@@ -34,8 +62,10 @@ bool record_mediacodec_worker_finish(record_mediacodec_worker_t *worker,
|
||||
/* One output action per encoder and one bounded PCM input action. Borrows
|
||||
* survive backpressure. No per-packet allocation, wait or GPU submission. */
|
||||
int record_mediacodec_worker_step(record_mediacodec_worker_t *worker);
|
||||
/* Controller still owns encoder/GPU retirement, mux cancellation and PCM
|
||||
* drain on failure. Free refuses active service unless aborted. */
|
||||
/* Controller still owns encoder/GPU retirement and PCM drain on failure.
|
||||
* Free refuses active service unless aborted, and retries output retirement
|
||||
* after failure, abort or completion. Keep output context and encoders alive
|
||||
* until free succeeds; only then may the controller stop/free the encoders. */
|
||||
void record_mediacodec_worker_abort(record_mediacodec_worker_t *worker);
|
||||
bool record_mediacodec_worker_free(record_mediacodec_worker_t *worker);
|
||||
RETRO_END_DECLS
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier: GPL-3.0-or-later */
|
||||
#ifndef RECORD_METAL_PLATFORM_H
|
||||
#define RECORD_METAL_PLATFORM_H
|
||||
#include <TargetConditionals.h>
|
||||
#include <Availability.h>
|
||||
|
||||
#if (TARGET_OS_OSX && defined(__MAC_OS_X_VERSION_MAX_ALLOWED) \
|
||||
&& __MAC_OS_X_VERSION_MAX_ALLOWED >= 101400) \
|
||||
|| (TARGET_OS_IPHONE && defined(__IPHONE_OS_VERSION_MAX_ALLOWED) \
|
||||
&& __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000) \
|
||||
|| (TARGET_OS_TV && defined(__TV_OS_VERSION_MAX_ALLOWED) \
|
||||
&& __TV_OS_VERSION_MAX_ALLOWED >= 130000)
|
||||
#define RECORD_METAL_NATIVE_SDK
|
||||
#endif
|
||||
|
||||
#define RECORD_METAL_NATIVE_AVAILABLE \
|
||||
__attribute__((availability(macos,introduced=10.14), \
|
||||
availability(ios,introduced=13.0), availability(tvos,introduced=13.0)))
|
||||
#define RECORD_METAL_NATIVE_RUNTIME \
|
||||
__builtin_available(macOS 10.14, iOS 13.0, tvOS 13.0, *)
|
||||
#endif
|
||||
@@ -0,0 +1,222 @@
|
||||
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier: GPL-3.0-or-later */
|
||||
#include "record_stream_mediacodec.h"
|
||||
#include <stdlib.h>
|
||||
|
||||
struct record_stream_mediacodec
|
||||
{
|
||||
record_mediacodec_encoder_t *video, *audio;
|
||||
net_rtmp_client_t *client;
|
||||
net_rtmp_session_t *session;
|
||||
rflv_mux_t *mux;
|
||||
record_mediacodec_stream_config_t config;
|
||||
const record_mediacodec_packet_t *pending;
|
||||
bool format[2], end[2];
|
||||
bool ready, sending, pending_audio, accepted, finishing, finished;
|
||||
bool failed, retired;
|
||||
};
|
||||
|
||||
static bool record_mcs_stream_track(void *context, bool audio, void *format)
|
||||
{
|
||||
record_stream_mediacodec_t *s = (record_stream_mediacodec_t*)context;
|
||||
unsigned at = audio ? 1 : 0;
|
||||
if (s->failed || s->retired || s->format[at] || !format
|
||||
|| (audio && !s->audio))
|
||||
return false;
|
||||
s->format[at] = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
static int record_mcs_stream_start(void *context)
|
||||
{
|
||||
record_stream_mediacodec_t *s = (record_stream_mediacodec_t*)context;
|
||||
if (s->failed || s->retired)
|
||||
return -1;
|
||||
if (!s->format[0] || !s->format[1] || !s->session)
|
||||
return 0;
|
||||
if (!s->mux)
|
||||
{
|
||||
s->mux = record_mediacodec_stream_mux_new(s->video, s->audio, &s->config);
|
||||
if (!s->mux)
|
||||
{
|
||||
s->failed = true;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return s->ready ? 1 : 0;
|
||||
}
|
||||
|
||||
static int record_mcs_stream_write(void *context, bool audio,
|
||||
const record_mediacodec_packet_t *packet)
|
||||
{
|
||||
record_stream_mediacodec_t *s = (record_stream_mediacodec_t*)context;
|
||||
int result;
|
||||
if (s->failed || s->retired || !s->ready || s->finishing || !packet
|
||||
|| s->end[audio ? 1 : 0] || (audio && !s->audio))
|
||||
return -1;
|
||||
if (s->pending)
|
||||
{
|
||||
if (s->pending != packet || s->pending_audio != audio || !s->accepted)
|
||||
return 0;
|
||||
s->pending = NULL;
|
||||
s->accepted = false;
|
||||
return 1;
|
||||
}
|
||||
if (!packet->size && (packet->flags & 4))
|
||||
return 1;
|
||||
if (!packet->data || !packet->size || packet->pts_us < 0 || (packet->flags & ~5u))
|
||||
return -1;
|
||||
if (!audio && s->config.codec != RECORD_MC_STREAM_AV1)
|
||||
result = rflv_mux_push_annexb(s->mux, packet->data, packet->size,
|
||||
(uint64_t)packet->pts_us, (uint64_t)packet->pts_us, !!(packet->flags & 1));
|
||||
else
|
||||
result = rflv_mux_push(s->mux, audio ? 2 : 1, packet->data, packet->size,
|
||||
(uint64_t)packet->pts_us, (uint64_t)packet->pts_us, !!(packet->flags & 1));
|
||||
if (result == RFLV_BUSY)
|
||||
return 0;
|
||||
if (result != RFLV_PROGRESS)
|
||||
return -1;
|
||||
s->pending = packet;
|
||||
s->pending_audio = audio;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static bool record_mcs_stream_end(void *context, bool audio, int64_t end_us)
|
||||
{
|
||||
record_stream_mediacodec_t *s = (record_stream_mediacodec_t*)context;
|
||||
if (s->failed || s->retired || end_us < 0
|
||||
|| (s->pending && s->pending_audio == audio))
|
||||
return false;
|
||||
s->end[audio ? 1 : 0] = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
static int record_mcs_stream_finish(void *context)
|
||||
{
|
||||
record_stream_mediacodec_t *s = (record_stream_mediacodec_t*)context;
|
||||
if (s->failed || s->retired || !s->end[0] || !s->end[1])
|
||||
return -1;
|
||||
if (s->pending || s->sending)
|
||||
return 0;
|
||||
if (!s->finishing)
|
||||
{
|
||||
if (!rflv_mux_finish(s->mux))
|
||||
return -1;
|
||||
if (!net_rtmp_session_finish(s->session))
|
||||
return 0;
|
||||
s->finishing = true;
|
||||
}
|
||||
return s->finished ? 1 : 0;
|
||||
}
|
||||
|
||||
static bool record_mcs_stream_retire(void *context)
|
||||
{
|
||||
record_stream_mediacodec_t *s = (record_stream_mediacodec_t*)context;
|
||||
if (s->retired)
|
||||
return true;
|
||||
net_rtmp_client_cancel(s->client);
|
||||
if (s->sending && !net_rtmp_session_media_release(s->session))
|
||||
return false;
|
||||
s->sending = false;
|
||||
rflv_mux_free(s->mux);
|
||||
s->mux = NULL;
|
||||
s->pending = NULL;
|
||||
s->retired = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
record_stream_mediacodec_t *record_stream_mediacodec_new(
|
||||
record_mediacodec_encoder_t *video, record_mediacodec_encoder_t *audio,
|
||||
net_rtmp_client_t *client, const record_mediacodec_stream_config_t *config)
|
||||
{
|
||||
record_stream_mediacodec_t *s;
|
||||
if (!video || !client || !config || !config->max_packet_size
|
||||
|| (unsigned)config->codec > RECORD_MC_STREAM_AV1)
|
||||
return NULL;
|
||||
s = (record_stream_mediacodec_t*)calloc(1, sizeof(*s));
|
||||
if (!s)
|
||||
return NULL;
|
||||
s->video = video;
|
||||
s->audio = audio;
|
||||
s->client = client;
|
||||
s->config = *config;
|
||||
s->format[1] = s->end[1] = !audio;
|
||||
return s;
|
||||
}
|
||||
|
||||
const record_mediacodec_output_ops_t *record_stream_mediacodec_ops(void)
|
||||
{
|
||||
static const record_mediacodec_output_ops_t ops = {
|
||||
record_mcs_stream_track, record_mcs_stream_start, record_mcs_stream_write,
|
||||
record_mcs_stream_end, record_mcs_stream_finish, record_mcs_stream_retire
|
||||
};
|
||||
return &ops;
|
||||
}
|
||||
|
||||
int record_stream_mediacodec_step(record_stream_mediacodec_t *s,
|
||||
uint32_t receive_time, net_rtmp_client_view_t *wait)
|
||||
{
|
||||
enum net_rtmp_client_state state;
|
||||
rflv_tag_t tag;
|
||||
net_rtmp_span_t spans[2];
|
||||
int result;
|
||||
if (!s || !wait || s->failed || s->retired)
|
||||
return -1;
|
||||
state = net_rtmp_client_step(s->client, receive_time, wait);
|
||||
if (state == NET_RTMP_CLIENT_FAILED)
|
||||
goto failed;
|
||||
if (state == NET_RTMP_CLIENT_FINISHED)
|
||||
{
|
||||
if (!s->finishing)
|
||||
goto failed;
|
||||
s->finished = true;
|
||||
return 2;
|
||||
}
|
||||
if (state != NET_RTMP_CLIENT_READY)
|
||||
return wait->again ? 1 : 0;
|
||||
s->session = wait->session;
|
||||
if (!s->session)
|
||||
goto failed;
|
||||
if (!s->mux || s->accepted)
|
||||
return wait->again ? 1 : 0;
|
||||
if (s->sending)
|
||||
{
|
||||
if (!net_rtmp_session_media_release(s->session))
|
||||
return wait->again ? 1 : 0;
|
||||
s->sending = false;
|
||||
if (!rflv_mux_release(s->mux))
|
||||
goto failed;
|
||||
s->accepted = s->pending != NULL;
|
||||
return 1;
|
||||
}
|
||||
result = rflv_mux_step(s->mux);
|
||||
if (result == RFLV_ERROR)
|
||||
goto failed;
|
||||
if (result == RFLV_PACKET)
|
||||
{
|
||||
if (!rflv_mux_get_tag(s->mux, &tag))
|
||||
return rflv_mux_release(s->mux) ? 1 : -1;
|
||||
spans[0].data = tag.spans[0].data;
|
||||
spans[0].size = tag.spans[0].size;
|
||||
spans[1].data = tag.spans[1].data;
|
||||
spans[1].size = tag.spans[1].size;
|
||||
if (net_rtmp_session_media(s->session, tag.type, tag.timestamp, spans, 2))
|
||||
{
|
||||
s->sending = true;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
else if (result == RFLV_BUSY)
|
||||
s->ready = true;
|
||||
return result == RFLV_PROGRESS || wait->again ? 1 : 0;
|
||||
failed:
|
||||
s->failed = true;
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool record_stream_mediacodec_free(record_stream_mediacodec_t *s)
|
||||
{
|
||||
if (s && !record_mcs_stream_retire(s))
|
||||
return false;
|
||||
free(s);
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier: GPL-3.0-or-later */
|
||||
#ifndef RECORD_STREAM_MEDIACODEC_H
|
||||
#define RECORD_STREAM_MEDIACODEC_H
|
||||
#include "record_mediacodec_worker.h"
|
||||
#include "record_mediacodec_stream_config.h"
|
||||
#include <net/net_rtmp_client.h>
|
||||
RETRO_BEGIN_DECLS
|
||||
typedef struct record_stream_mediacodec record_stream_mediacodec_t;
|
||||
/* Cold I/O owner. Copies config; borrows encoders and client until free.
|
||||
* The controller must establish stream priming/source epochs separately;
|
||||
* file calibration prefixes and MP4 edits must not be used for this output.
|
||||
* No producer stop/free until worker free has retired this output. */
|
||||
record_stream_mediacodec_t *record_stream_mediacodec_new(
|
||||
record_mediacodec_encoder_t *video, record_mediacodec_encoder_t *audio,
|
||||
net_rtmp_client_t *client, const record_mediacodec_stream_config_t *config);
|
||||
const record_mediacodec_output_ops_t *record_stream_mediacodec_ops(void);
|
||||
/* One bounded network/mux action, before worker_step. -1 error, 0 BUSY,
|
||||
* 1 progress, 2 completed. Caller schedules returned socket interests and
|
||||
* owns setup/drain deadlines. No waits, internal clocks or packet allocation. */
|
||||
int record_stream_mediacodec_step(record_stream_mediacodec_t *stream,
|
||||
uint32_t receive_time, net_rtmp_client_view_t *wait);
|
||||
/* After worker_free succeeds. Retires cancellation before freeing mux bytes;
|
||||
* false requires another retry. Does not destroy borrowed client/encoders. */
|
||||
bool record_stream_mediacodec_free(record_stream_mediacodec_t *stream);
|
||||
RETRO_END_DECLS
|
||||
#endif
|
||||
@@ -317,6 +317,8 @@ int main(void)
|
||||
assert(record_mediacodec_encoder_pump(s, &packet) == RECORD_MC_FORMAT);
|
||||
assert(record_mediacodec_encoder_audio_config(s, &config_data,
|
||||
&config_bytes) && config_data == asc && config_bytes == 2);
|
||||
assert(!record_mediacodec_encoder_video_config(s, 0, &config_data,
|
||||
&config_bytes) && !config_data && !config_bytes);
|
||||
for (config_fault = 1; config_fault <= 3; config_fault++)
|
||||
assert(!record_mediacodec_encoder_audio_config(s, &config_data,
|
||||
&config_bytes) && !config_data && !config_bytes);
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "../../../record/record_mediacodec_worker.h"
|
||||
#include "../../../record/record_aac_stream_timing.h"
|
||||
|
||||
struct record_mediacodec_encoder
|
||||
{
|
||||
@@ -26,6 +27,9 @@ static const unsigned char config[] = {0x11, 0x88};
|
||||
static const unsigned char encoded = 1;
|
||||
static unsigned fault;
|
||||
static bool empty;
|
||||
static bool live_mode;
|
||||
static unsigned live_delay;
|
||||
static record_aac_stream_timing_t live_timing;
|
||||
|
||||
const int16_t *record_aac_probe_input(record_aac_probe_t *p,
|
||||
size_t at, size_t *frames)
|
||||
@@ -63,7 +67,7 @@ bool record_aac_probe_delay(const record_aac_probe_t *p, unsigned *delay)
|
||||
{
|
||||
if (!p->ready)
|
||||
return false;
|
||||
*delay = fault == 7 ? 4096 : 2048;
|
||||
*delay = live_mode ? live_delay : fault == 7 ? 4096 : 2048;
|
||||
return true;
|
||||
}
|
||||
bool record_mediacodec_encoder_audio_config(record_mediacodec_encoder_t *e,
|
||||
@@ -86,6 +90,9 @@ int record_mediacodec_encoder_audio_submit(record_mediacodec_encoder_t *e,
|
||||
return 0;
|
||||
if (timestamp < RECORD_AAC_PROBE_FRAMES)
|
||||
assert(samples == prefix + timestamp);
|
||||
else if (live_mode && timestamp < live_timing.input_epoch)
|
||||
for (i = 0; i < count; i++)
|
||||
assert(samples[i] == 0);
|
||||
else
|
||||
for (i = 0; i < count; i++)
|
||||
assert(samples[i] == 7);
|
||||
@@ -96,7 +103,7 @@ int record_mediacodec_encoder_audio_submit(record_mediacodec_encoder_t *e,
|
||||
int record_mediacodec_encoder_audio_finish(record_mediacodec_encoder_t *e,
|
||||
uint64_t end)
|
||||
{
|
||||
assert(e->audio && end == RECORD_AAC_PROBE_FRAMES + (empty ? 0 : 17)
|
||||
assert(e->audio && end == (live_mode ? live_timing.input_epoch : RECORD_AAC_PROBE_FRAMES) + (empty ? 0 : 17)
|
||||
&& e->accepted == end);
|
||||
e->eos = true;
|
||||
return 1;
|
||||
@@ -117,8 +124,10 @@ int record_mediacodec_encoder_pump(record_mediacodec_encoder_t *e,
|
||||
return RECORD_MC_FORMAT;
|
||||
}
|
||||
limit = e->audio ? (unsigned)(e->accepted / 1024) : 1;
|
||||
if (e->audio && live_mode)
|
||||
limit = e->accepted > live_delay ? (unsigned)((e->accepted - live_delay) / 1024) : 0;
|
||||
if (e->audio && e->eos)
|
||||
limit = fault == 5 ? 7 : empty ? 8 : 11;
|
||||
limit = fault == 5 ? 7 : live_mode ? (unsigned)((e->accepted + live_delay + 1023) / 1024) : empty ? 8 : 11;
|
||||
if (e->packets < limit)
|
||||
{
|
||||
e->held = true;
|
||||
@@ -159,14 +168,15 @@ bool record_mux_mediacodec_write(record_mux_mediacodec_t *m, bool audio,
|
||||
assert(p->data == &encoded && p->size == 1);
|
||||
assert(p->pts_us == (audio
|
||||
? (int64_t)((uint64_t)m->packets[1] * 1024 * 1000000 / 48000)
|
||||
: 0));
|
||||
: live_mode ? live_timing.video_epoch_us : 0));
|
||||
m->packets[audio]++;
|
||||
return true;
|
||||
}
|
||||
bool record_mux_mediacodec_end(record_mux_mediacodec_t *m,
|
||||
bool audio, int64_t end)
|
||||
{
|
||||
assert(end == (audio ? (empty ? 170666 : 234666) : 1000000));
|
||||
assert(end == (audio ? live_mode ? (int64_t)((uint64_t)(2048 + (empty ? 0 : 17)) * 1000000 / 48000)
|
||||
: (empty ? 170666 : 234666) : 1000000 + (live_mode ? live_timing.video_epoch_us : 0)));
|
||||
m->ends[audio]++;
|
||||
return true;
|
||||
}
|
||||
@@ -176,12 +186,16 @@ int record_mux_mediacodec_finish(record_mux_mediacodec_t *m)
|
||||
m->finished = true;
|
||||
return 1;
|
||||
}
|
||||
static void exercise(unsigned failure, bool cancel, bool no_pcm)
|
||||
#include "worker_output_fixture.h"
|
||||
|
||||
static void exercise_mode(unsigned failure, bool cancel, bool no_pcm, bool backpressure,
|
||||
bool live, unsigned delay)
|
||||
{
|
||||
record_mediacodec_encoder_t video, audio;
|
||||
record_mux_mediacodec_t mux;
|
||||
record_aac_probe_t probe;
|
||||
record_mediacodec_worker_t *w;
|
||||
struct record_busy_output output;
|
||||
record_audio_queue_t *q;
|
||||
int16_t pcm[17];
|
||||
const int16_t *samples;
|
||||
@@ -196,6 +210,9 @@ static void exercise(unsigned failure, bool cancel, bool no_pcm)
|
||||
audio.audio = true;
|
||||
fault = failure;
|
||||
empty = no_pcm;
|
||||
live_mode = live;
|
||||
live_delay = delay;
|
||||
assert(record_aac_stream_timing(48000, delay, &live_timing));
|
||||
for (i = 0; i < 17; i++)
|
||||
pcm[i] = 7;
|
||||
q = record_audio_queue_new(1, 17, 256);
|
||||
@@ -203,9 +220,17 @@ static void exercise(unsigned failure, bool cancel, bool no_pcm)
|
||||
if (!empty)
|
||||
assert(record_audio_queue_push(q, pcm, 17, 0) == RECORD_AUDIO_OK);
|
||||
assert(record_audio_queue_finish(q));
|
||||
w = record_mediacodec_worker_new(&video, &audio, q, &mux, 1, 48000);
|
||||
assert(w && record_mediacodec_worker_calibrate(w, &probe));
|
||||
memset(&output, 0, sizeof(output));
|
||||
output.mux = &mux;
|
||||
output.busy[0] = 2;
|
||||
output.busy[1] = 4;
|
||||
w = backpressure
|
||||
? record_mediacodec_worker_new_output(&video, &audio, q, &busy_ops, &output, 1, 48000)
|
||||
: record_mediacodec_worker_new(&video, &audio, q, &mux, 1, 48000);
|
||||
assert(w && (live ? record_mediacodec_worker_calibrate_live(w, &probe)
|
||||
: record_mediacodec_worker_calibrate(w, &probe)));
|
||||
assert(!record_mediacodec_worker_calibrate(w, &probe));
|
||||
assert(!record_mediacodec_worker_calibrate_live(w, &probe));
|
||||
assert(!record_mediacodec_worker_audio_edit(w, &skip, &source, &roll));
|
||||
assert(!record_mediacodec_worker_finish(w, 1000000, (uint64_t)-1));
|
||||
assert(record_mediacodec_worker_finish(w, 1000000, empty ? 0 : 17));
|
||||
@@ -228,10 +253,20 @@ static void exercise(unsigned failure, bool cancel, bool no_pcm)
|
||||
else
|
||||
{
|
||||
assert(result == RECORD_MC_WORKER_END && mux.finished);
|
||||
assert(record_mediacodec_worker_audio_edit(w, &skip, &source, &roll));
|
||||
assert(skip == 10240 && source == (empty ? 0 : 17) && roll == 2);
|
||||
assert(audio.accepted == (empty ? 8192 : 8209)
|
||||
&& mux.packets[1] == (empty ? 8 : 11));
|
||||
if (live)
|
||||
{
|
||||
assert(!record_mediacodec_worker_audio_edit(w, &skip, &source, &roll));
|
||||
assert(audio.accepted == live_timing.input_epoch + (empty ? 0 : 17));
|
||||
assert(mux.packets[1] == (empty ? 2u : 3u));
|
||||
assert(audio.packets - mux.packets[1] == live_timing.coded_origin / 1024);
|
||||
}
|
||||
else
|
||||
{
|
||||
assert(record_mediacodec_worker_audio_edit(w, &skip, &source, &roll));
|
||||
assert(skip == 10240 && source == (empty ? 0 : 17) && roll == 2);
|
||||
assert(audio.accepted == (empty ? 8192 : 8209)
|
||||
&& mux.packets[1] == (empty ? 8 : 11));
|
||||
}
|
||||
}
|
||||
assert(record_mediacodec_worker_step(w) == result);
|
||||
assert(record_mediacodec_worker_free(w));
|
||||
@@ -240,17 +275,33 @@ static void exercise(unsigned failure, bool cancel, bool no_pcm)
|
||||
assert(record_audio_queue_ack(q));
|
||||
assert(record_audio_queue_free(q));
|
||||
}
|
||||
static void exercise(unsigned failure, bool cancel, bool no_pcm, bool backpressure)
|
||||
{
|
||||
exercise_mode(failure, cancel, no_pcm, backpressure, false, 2048);
|
||||
}
|
||||
int main(void)
|
||||
{
|
||||
exercise(0, false, false);
|
||||
exercise(0, false, true);
|
||||
exercise(2, false, false);
|
||||
exercise(3, false, false);
|
||||
exercise(4, false, false);
|
||||
exercise(5, false, false);
|
||||
exercise(7, false, false);
|
||||
exercise(0, true, false);
|
||||
static const unsigned delays[] = {0,1,1023,1024,2048,4096};
|
||||
unsigned i;
|
||||
exercise(0, false, false, false);
|
||||
exercise(0, false, true, false);
|
||||
exercise(2, false, false, false);
|
||||
exercise(3, false, false, false);
|
||||
exercise(4, false, false, false);
|
||||
exercise(5, false, false, false);
|
||||
exercise(7, false, false, false);
|
||||
exercise(0, true, false, false);
|
||||
exercise(0, false, false, true);
|
||||
exercise(0, false, true, true);
|
||||
for (i = 0; i < sizeof(delays) / sizeof(delays[0]); i++)
|
||||
{
|
||||
exercise_mode(0, false, false, false, true, delays[i]);
|
||||
exercise_mode(0, false, true, false, true, delays[i]);
|
||||
exercise_mode(0, false, false, true, true, delays[i]);
|
||||
exercise_mode(0, false, true, true, true, delays[i]);
|
||||
exercise_mode(0, true, false, true, true, delays[i]);
|
||||
}
|
||||
puts("PASS: calibrated worker prefix backpressure, coded/source clocks, "
|
||||
"trim ownership, early EOS and cancellation; codecs/probe mocked");
|
||||
"file trim, live silent priming, source epochs, empty PCM, early EOS and cancellation; codecs/probe mocked");
|
||||
return 0;
|
||||
}
|
||||
@@ -44,7 +44,7 @@ with tempfile.TemporaryDirectory(prefix="mediacodec-encode-") as directory:
|
||||
audio_fragment.append(shipping_audio[start:end])
|
||||
(Path(directory) / "audio_tap_fragment.c").write_text("\n".join(audio_fragment))
|
||||
binary = Path(directory) / ("encoder_test.exe" if os.name == "nt" else "encoder_test")
|
||||
for source, extra in (("test.c", []), ("audio_test.c", []),
|
||||
for source, extra in (("test.c", []), ("stream_config_test.c", []), ("stream_output_test.c", []), ("stream_timing_test.c", []), ("audio_test.c", []),
|
||||
("mux_test.c", ["-DRECORD_MEDIACODEC_MUX_TEST"]),
|
||||
("worker_test.c", []), ("calibrated_worker_test.c", []),
|
||||
("surface_test.c", []),
|
||||
@@ -60,9 +60,17 @@ with tempfile.TemporaryDirectory(prefix="mediacodec-encode-") as directory:
|
||||
("session_test.c", []), ("dispatch_test.c", []),
|
||||
("gpu_dispatch_test.c", [])):
|
||||
command = compiler + flags + extra + [str(here / source), "-o", str(binary)]
|
||||
if source == "stream_timing_test.c":
|
||||
command += [str(root / "record/record_aac_stream_timing.c")]
|
||||
if source in ("stream_config_test.c", "stream_output_test.c"):
|
||||
command += [str(root / "record/record_mediacodec_stream_config.c"),
|
||||
str(root / "libretro-common/formats/flv/rflv_mux.c")]
|
||||
if source == "stream_output_test.c":
|
||||
command += [str(root / "record/record_stream_mediacodec.c")]
|
||||
if source in ("worker_test.c", "calibrated_worker_test.c"):
|
||||
command += ["-DHAVE_THREADS",
|
||||
str(root / "record/record_mediacodec_worker.c"),
|
||||
str(root / "record/record_aac_stream_timing.c"),
|
||||
str(root / "record/record_audio_queue.c"),
|
||||
str(root / "libretro-common/queues/retro_spsc.c")]
|
||||
if source == "worker_test.c":
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier: GPL-3.0-or-later */
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "../../../record/record_mediacodec_stream_config.h"
|
||||
|
||||
struct record_mediacodec_encoder
|
||||
{
|
||||
const void *data[2];
|
||||
size_t bytes[2];
|
||||
bool audio;
|
||||
};
|
||||
static unsigned queries;
|
||||
static uint8_t sps[] = {0,0,0,1,103,66,0,30};
|
||||
static uint8_t pps[] = {0,0,0,1,104};
|
||||
static uint8_t asc[] = {18,16};
|
||||
static const uint8_t combined_avc[] = {0,0,0,1,103,66,0,30,0,0,0,1,104};
|
||||
static const uint8_t avcc[] = {1,66,0,30,255,225,0,4,103,66,0,30,1,0,1,104};
|
||||
/* Golden parameter-set bytes from the shared FLV mux HEVC fixture. */
|
||||
static const uint8_t hevc8[] =
|
||||
{
|
||||
0,0,0,1,64,1,170,0,0,0,1,66,
|
||||
1,1,17,17,17,17,17,17,17,17,17,17,
|
||||
17,17,160,16,32,97,112,0,0,1,68,1,
|
||||
170
|
||||
};
|
||||
static const uint8_t hevc10[] =
|
||||
{
|
||||
0,0,0,1,64,1,170,0,0,0,1,66,
|
||||
1,1,17,17,17,17,17,17,17,17,17,17,
|
||||
17,17,160,16,32,97,55,0,0,1,68,1,
|
||||
170
|
||||
};
|
||||
static const uint8_t av1[] = {129,0,76,0,10,13,0,0,0,3,55,251,230,215,206,129,1,4,130};
|
||||
bool record_mediacodec_encoder_video_config(record_mediacodec_encoder_t *encoder,
|
||||
unsigned index, const void **data, size_t *bytes)
|
||||
{
|
||||
queries++;
|
||||
*data = NULL;
|
||||
*bytes = 0;
|
||||
if (!encoder || encoder->audio || index > 1 || !encoder->bytes[index])
|
||||
return false;
|
||||
*data = encoder->data[index];
|
||||
*bytes = encoder->bytes[index];
|
||||
return true;
|
||||
}
|
||||
bool record_mediacodec_encoder_audio_config(record_mediacodec_encoder_t *encoder,
|
||||
const void **data, size_t *bytes)
|
||||
{
|
||||
queries++;
|
||||
*data = NULL;
|
||||
*bytes = 0;
|
||||
if (!encoder || !encoder->audio || !encoder->bytes[0])
|
||||
return false;
|
||||
*data = encoder->data[0];
|
||||
*bytes = encoder->bytes[0];
|
||||
return true;
|
||||
}
|
||||
static void setup(record_mediacodec_encoder_t *video, record_mediacodec_encoder_t *audio,
|
||||
record_mediacodec_stream_config_t *config)
|
||||
{
|
||||
memset(video, 0, sizeof(*video));
|
||||
memset(audio, 0, sizeof(*audio));
|
||||
memset(config, 0, sizeof(*config));
|
||||
video->data[0] = sps;
|
||||
video->bytes[0] = sizeof(sps);
|
||||
video->data[1] = pps;
|
||||
video->bytes[1] = sizeof(pps);
|
||||
audio->data[0] = asc;
|
||||
audio->bytes[0] = sizeof(asc);
|
||||
audio->audio = true;
|
||||
config->max_packet_size = 1024;
|
||||
config->bit_depth = 8;
|
||||
}
|
||||
static void drain_setup(rflv_mux_t *mux)
|
||||
{
|
||||
const rflv_span_t *spans;
|
||||
size_t count;
|
||||
unsigned actions = 0;
|
||||
while (rflv_mux_get(mux, &spans, &count))
|
||||
{
|
||||
assert(++actions < 8);
|
||||
assert(rflv_mux_release(mux));
|
||||
}
|
||||
assert(rflv_mux_step(mux) == RFLV_BUSY);
|
||||
}
|
||||
int main(void)
|
||||
{
|
||||
record_mediacodec_encoder_t video, audio;
|
||||
record_mediacodec_stream_config_t config;
|
||||
rflv_mux_t *mux;
|
||||
rflv_tag_t tag;
|
||||
unsigned before, i;
|
||||
static const uint8_t picture[] = {0,0,1,101,136};
|
||||
setup(&video, &audio, &config);
|
||||
mux = record_mediacodec_stream_mux_new(&video, &audio, &config);
|
||||
assert(mux && rflv_mux_release(mux));
|
||||
sps[4] = pps[4] = asc[0] = 0; /* Native setup may change after constructor return. */
|
||||
assert(rflv_mux_get_tag(mux, &tag) && tag.type == 9);
|
||||
assert(tag.spans[1].size == sizeof(avcc) && !memcmp(tag.spans[1].data, avcc, sizeof(avcc)));
|
||||
assert(rflv_mux_release(mux) && rflv_mux_get_tag(mux, &tag) && tag.type == 8);
|
||||
assert(tag.spans[1].size == 2 && ((const uint8_t*)tag.spans[1].data)[0] == 18);
|
||||
drain_setup(mux);
|
||||
assert(rflv_mux_push_annexb(mux, picture, sizeof(picture), 1000000, 1000000, true) == RFLV_PROGRESS);
|
||||
for (i = 0; i < 8 && rflv_mux_step(mux) == RFLV_PROGRESS; i++) {}
|
||||
assert(i < 8 && rflv_mux_get_tag(mux, &tag) && tag.timestamp == 1000);
|
||||
assert(rflv_mux_release(mux));
|
||||
assert(rflv_mux_push(mux, 2, picture, 2, 1000000, 1000000, false) == RFLV_PROGRESS);
|
||||
assert(rflv_mux_step(mux) == RFLV_PACKET && rflv_mux_get_tag(mux, &tag));
|
||||
assert(tag.timestamp == 1000 && tag.spans[1].data == picture);
|
||||
rflv_mux_free(mux);
|
||||
sps[4] = 103;
|
||||
pps[4] = 104;
|
||||
asc[0] = 18;
|
||||
setup(&video, &audio, &config);
|
||||
audio.bytes[0] = 0;
|
||||
assert(!record_mediacodec_stream_mux_new(&video, &audio, &config));
|
||||
video.bytes[1] = 0;
|
||||
assert(!record_mediacodec_stream_mux_new(&video, NULL, &config));
|
||||
video.data[0] = combined_avc;
|
||||
video.bytes[0] = sizeof(combined_avc);
|
||||
mux = record_mediacodec_stream_mux_new(&video, NULL, &config);
|
||||
assert(mux);
|
||||
drain_setup(mux);
|
||||
rflv_mux_free(mux);
|
||||
setup(&video, &audio, &config);
|
||||
before = queries;
|
||||
config.max_packet_size = 0xffffffu;
|
||||
assert(!record_mediacodec_stream_mux_new(&video, NULL, &config) && queries == before);
|
||||
config.max_packet_size = 1024;
|
||||
config.bit_depth = 10;
|
||||
assert(!record_mediacodec_stream_mux_new(&video, NULL, &config) && queries == before);
|
||||
setup(&video, &audio, &config);
|
||||
config.codec = RECORD_MC_STREAM_HEVC;
|
||||
video.data[0] = hevc8;
|
||||
video.bytes[0] = sizeof(hevc8);
|
||||
mux = record_mediacodec_stream_mux_new(&video, NULL, &config);
|
||||
assert(mux);
|
||||
drain_setup(mux);
|
||||
rflv_mux_free(mux);
|
||||
video.data[0] = hevc10;
|
||||
video.bytes[0] = sizeof(hevc10);
|
||||
assert(!record_mediacodec_stream_mux_new(&video, NULL, &config));
|
||||
config.bit_depth = 10;
|
||||
mux = record_mediacodec_stream_mux_new(&video, &audio, &config);
|
||||
assert(mux);
|
||||
drain_setup(mux);
|
||||
rflv_mux_free(mux);
|
||||
config.codec = RECORD_MC_STREAM_AV1;
|
||||
video.data[0] = av1;
|
||||
video.bytes[0] = sizeof(av1);
|
||||
mux = record_mediacodec_stream_mux_new(&video, NULL, &config);
|
||||
assert(mux);
|
||||
drain_setup(mux);
|
||||
rflv_mux_free(mux);
|
||||
config.bit_depth = 8;
|
||||
assert(!record_mediacodec_stream_mux_new(&video, NULL, &config));
|
||||
for (i = RECORD_MC_STREAM_AVC; i <= RECORD_MC_STREAM_AV1; i++)
|
||||
{
|
||||
config.codec = (enum record_mediacodec_stream_codec)i;
|
||||
config.bit_depth = 8;
|
||||
config.max_packet_size = i == RECORD_MC_STREAM_HEVC ? 0xfffff8u : 0xfffffbu;
|
||||
before = queries;
|
||||
assert(!record_mediacodec_stream_mux_new(&video, NULL, &config) && queries == before);
|
||||
}
|
||||
puts("PASS MediaCodec AVC/HEVC/AV1/AAC setup, copied configuration, depth admission and microsecond mux clocks");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier: GPL-3.0-or-later */
|
||||
#ifdef NDEBUG
|
||||
#error Output ownership checks require assertions
|
||||
#endif
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "../../../record/record_stream_mediacodec.h"
|
||||
struct record_mediacodec_encoder { bool audio; };
|
||||
struct net_rtmp_client { int unused; };
|
||||
struct net_rtmp_session { int unused; };
|
||||
static struct net_rtmp_session session;
|
||||
static bool credit, live, cancelled, closing, network_fail, connecting;
|
||||
static unsigned messages, releases, retire_delay;
|
||||
static const void *borrowed;
|
||||
static const uint8_t csd[] = {0,0,0,1,103,66,0,30,0,0,0,1,104};
|
||||
static const uint8_t asc[] = {18,16};
|
||||
static uint8_t aac[] = {1,2,3,4};
|
||||
static const uint8_t picture[] = {0,0,0,1,101,1,2};
|
||||
bool record_mediacodec_encoder_video_config(record_mediacodec_encoder_t *s,
|
||||
unsigned index, const void **data, size_t *size)
|
||||
{
|
||||
*data = NULL; *size = 0;
|
||||
if (!s || s->audio || index) return false;
|
||||
*data = csd; *size = sizeof(csd); return true;
|
||||
}
|
||||
bool record_mediacodec_encoder_audio_config(record_mediacodec_encoder_t *s,
|
||||
const void **data, size_t *size)
|
||||
{
|
||||
*data = NULL; *size = 0;
|
||||
if (!s || !s->audio) return false;
|
||||
*data = asc; *size = sizeof(asc); return true;
|
||||
}
|
||||
enum net_rtmp_client_state net_rtmp_client_step(net_rtmp_client_t *c,
|
||||
uint32_t time, net_rtmp_client_view_t *view)
|
||||
{
|
||||
(void)c; (void)time;
|
||||
memset(view, 0, sizeof(*view)); view->session = &session; view->fd = 7;
|
||||
view->write = live;
|
||||
if (network_fail) return NET_RTMP_CLIENT_FAILED;
|
||||
if (connecting) return NET_RTMP_CLIENT_CONNECTING;
|
||||
return closing ? NET_RTMP_CLIENT_FINISHED : NET_RTMP_CLIENT_READY;
|
||||
}
|
||||
void net_rtmp_client_cancel(net_rtmp_client_t *c)
|
||||
{
|
||||
(void)c; cancelled = true;
|
||||
}
|
||||
bool net_rtmp_session_media(net_rtmp_session_t *s, unsigned type,
|
||||
uint32_t time, const net_rtmp_span_t *spans, size_t count)
|
||||
{
|
||||
(void)s;
|
||||
assert(count == 2 && (type == 8 || type == 9));
|
||||
if (live || !credit) return false;
|
||||
if (messages >= 2) assert(time == 1000);
|
||||
if (type == 8 && messages >= 2) assert(spans[1].data == aac);
|
||||
borrowed = spans[1].data;
|
||||
live = true; messages++; return true;
|
||||
}
|
||||
bool net_rtmp_session_media_release(net_rtmp_session_t *s)
|
||||
{
|
||||
(void)s;
|
||||
if (!live) return false;
|
||||
if (cancelled && retire_delay) { retire_delay--; return false; }
|
||||
if (!cancelled && !credit) return false;
|
||||
assert(borrowed); live = false; borrowed = NULL; releases++; return true;
|
||||
}
|
||||
bool net_rtmp_session_finish(net_rtmp_session_t *s)
|
||||
{
|
||||
(void)s; assert(!live); closing = true; return true;
|
||||
}
|
||||
static void run(bool fail, bool audio_enabled)
|
||||
{
|
||||
record_mediacodec_encoder_t video = {false}, audio = {true};
|
||||
struct net_rtmp_client client;
|
||||
record_mediacodec_stream_config_t config;
|
||||
record_stream_mediacodec_t *stream;
|
||||
const record_mediacodec_output_ops_t *ops = record_stream_mediacodec_ops();
|
||||
record_mediacodec_packet_t packet, other;
|
||||
net_rtmp_client_view_t wait;
|
||||
unsigned i;
|
||||
memset(&config, 0, sizeof(config)); config.max_packet_size = 1024; config.bit_depth = 8;
|
||||
credit = true; live = cancelled = closing = network_fail = false;
|
||||
connecting = true; messages = releases = retire_delay = 0;
|
||||
stream = record_stream_mediacodec_new(&video, audio_enabled ? &audio : NULL, &client, &config);
|
||||
assert(stream && !ops->start(stream));
|
||||
assert(ops->track(stream, false, &video));
|
||||
assert(!ops->track(stream, false, &video));
|
||||
if (audio_enabled) assert(ops->track(stream, true, &audio));
|
||||
else assert(!ops->track(stream, true, &audio));
|
||||
assert(record_stream_mediacodec_step(stream, 0, &wait) == 0);
|
||||
assert(!ops->start(stream)); connecting = false;
|
||||
for (i = 0; i < 40 && !ops->start(stream); i++)
|
||||
assert(record_stream_mediacodec_step(stream, 0, &wait) >= 0);
|
||||
assert(i < 40 && messages == (audio_enabled ? 2u : 1u));
|
||||
memset(&packet, 0, sizeof(packet));
|
||||
packet.pts_us = 1000000; packet.data = audio_enabled ? aac : picture;
|
||||
packet.size = audio_enabled ? sizeof(aac) : sizeof(picture); packet.flags = 1;
|
||||
other = packet;
|
||||
assert(!ops->write(stream, audio_enabled, &packet));
|
||||
assert(!ops->write(stream, audio_enabled, &other));
|
||||
for (i = 0; i < 40 && !live; i++)
|
||||
assert(record_stream_mediacodec_step(stream, 0, &wait) >= 0);
|
||||
assert(live && i < 40);
|
||||
credit = false;
|
||||
for (i = 0; i < 4; i++)
|
||||
{
|
||||
assert(record_stream_mediacodec_step(stream, 0, &wait) == 0);
|
||||
assert(!ops->write(stream, audio_enabled, &packet)); assert(live);
|
||||
}
|
||||
if (fail)
|
||||
{
|
||||
network_fail = true;
|
||||
assert(record_stream_mediacodec_step(stream, 0, &wait) == -1);
|
||||
retire_delay = 2;
|
||||
assert(!ops->retire(stream) && live);
|
||||
assert(!record_stream_mediacodec_free(stream) && live);
|
||||
assert(ops->retire(stream) && !live && cancelled);
|
||||
}
|
||||
else
|
||||
{
|
||||
credit = true;
|
||||
assert(record_stream_mediacodec_step(stream, 0, &wait) == 1);
|
||||
assert(!live && ops->write(stream, audio_enabled, &packet) == 1);
|
||||
assert(ops->end(stream, false, 2000000));
|
||||
if (audio_enabled) assert(ops->end(stream, true, 2000000));
|
||||
assert(ops->finish(stream) == 0);
|
||||
assert(record_stream_mediacodec_step(stream, 0, &wait) == 2);
|
||||
assert(ops->finish(stream) == 1);
|
||||
}
|
||||
assert(record_stream_mediacodec_free(stream));
|
||||
assert(messages == releases);
|
||||
}
|
||||
int main(void)
|
||||
{
|
||||
run(false, true); run(false, false); run(true, true); run(true, false);
|
||||
puts("PASS: real FLV setup and packet conversion, borrowed AAC, network backpressure, closure and delayed cancellation retirement; transport mocked");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Independent AAC-LC packet-cut experiment; not MediaCodec device acceptance.
|
||||
|
||||
Requires CC and FFmpeg. Compiles the actual timing policy, then measures encoder
|
||||
lag from a chirp and compares independently decoded source after silent preroll.
|
||||
"""
|
||||
from pathlib import Path
|
||||
import array,json,math,os,shlex,shutil,subprocess,tempfile
|
||||
ff=shutil.which('ffmpeg');assert ff
|
||||
results=[]
|
||||
def decode(path,channels):
|
||||
p=subprocess.run([ff,'-v','error','-i',str(path),'-f','f32le','-'],check=True,stdout=subprocess.PIPE)
|
||||
a=array.array('f');a.frombytes(p.stdout);return a
|
||||
with tempfile.TemporaryDirectory(prefix='aac-live-prime-') as tmp:
|
||||
d=Path(tmp)
|
||||
here=Path(__file__).resolve().parent; root=here.parents[2]
|
||||
binary=d/('timing.exe' if os.name=='nt' else 'timing')
|
||||
subprocess.run(shlex.split(os.environ.get('CC','cc'))+['-O1','-std=c89','-pedantic','-Wall','-Wextra','-Werror','-Wno-long-long','-Wframe-larger-than=2048','-I'+str(root/'libretro-common/include'),str(here/'stream_timing_test.c'),str(root/'record/record_aac_stream_timing.c'),'-o',str(binary)],check=True)
|
||||
timing={int(row[0]):list(map(int,row[1:])) for row in (line.split() for line in subprocess.check_output([str(binary),'--epochs'],text=True).splitlines())}
|
||||
for rate in (44100,48000):
|
||||
for channels in (1,2):
|
||||
# FFmpeg AAC-LC has a measured 1024-frame delay in this experiment only.
|
||||
# Android delay remains runtime-calibrated; do not promote this constant.
|
||||
delay=1024; prefix=8192; guard=4096; preroll=2048
|
||||
source_input,origin,preroll,video_epoch=timing[rate]
|
||||
source_decoded=source_input+delay
|
||||
data=array.array('h')
|
||||
for i in range(source_input+8192):
|
||||
for c in range(channels):
|
||||
value=0
|
||||
if 2048 <= i < 4096:
|
||||
t=(i-2048)/2048; value=int(12000*math.sin(math.pi*t)**2*math.sin(2*math.pi*(400+300*c)*(i-2048)/rate))
|
||||
if source_input <= i:
|
||||
at=i-source_input;value=int(10000*math.sin(2*math.pi*(997+701*c)*at/rate))
|
||||
data.append(value)
|
||||
raw=d/'input.pcm';raw.write_bytes(data.tobytes());encoded=d/'full.aac'
|
||||
subprocess.run([ff,'-v','error','-f','s16le','-ar',str(rate),'-ac',str(channels),'-i',str(raw),'-c:a','aac','-b:a','192k','-f','adts',str(encoded),'-y'],check=True)
|
||||
packet=[];blob=encoded.read_bytes();at=0
|
||||
while at<len(blob):
|
||||
assert blob[at]==255 and blob[at+1]&240==240
|
||||
size=((blob[at+3]&3)<<11)|(blob[at+4]<<3)|(blob[at+5]>>5)
|
||||
assert size>=7 and at+size<=len(blob)
|
||||
packet.append(blob[at:at+size]);at+=size
|
||||
stream=d/'live.aac';stream.write_bytes(b''.join(packet[origin//1024:]))
|
||||
ref=decode(encoded,channels);live=decode(stream,channels)
|
||||
# Recover delay independently from the encoded chirp, rather than trusting
|
||||
# the encoder-specific input assumption used to construct this trial.
|
||||
errors=[]
|
||||
reference=[data[i*channels]/32768 for i in range(2048,4096)]
|
||||
for candidate in range(4097):
|
||||
error=sum((ref[(2048+candidate+i)*channels]-reference[i])**2 for i in range(2048))
|
||||
errors.append(error)
|
||||
measured=min(range(len(errors)),key=errors.__getitem__)
|
||||
assert measured==delay,(rate,channels,measured,delay)
|
||||
assert errors[measured]<sum(x*x for x in reference)*.02
|
||||
expected=ref[(origin+preroll)*channels:(origin+preroll+8192)*channels]
|
||||
actual=live[preroll*channels:(preroll+8192)*channels]
|
||||
assert len(actual)==len(expected)==8192*channels
|
||||
delta=max(abs(a-b) for a,b in zip(actual,expected))
|
||||
silence=max(abs(x) for x in live[:1024*channels])
|
||||
energy=sum(x*x for x in actual)/len(actual)
|
||||
assert delta < 0.0001,(rate,channels,delta)
|
||||
assert silence < 0.0001,(rate,channels,silence)
|
||||
assert energy > .02
|
||||
# Removing the offset must put the beginning in preroll, not source time.
|
||||
assert sum(x*x for x in live[:1024*channels])/(1024*channels)<1e-8
|
||||
results.append(dict(rate=rate,channels=channels,measured_encoder_delay=measured,input_epoch=source_input,decoded_epoch=source_decoded,drop_frames=origin,preroll=preroll,max_source_error=delta,initial_silence_peak=silence,source_energy=energy))
|
||||
Path('aac-live-priming-experiment.json').write_text(json.dumps(dict(scope='FFmpeg AAC-LC synthetic encoded-packet experiment; not native MediaCodec validation',cases=results),indent=2)+'\n')
|
||||
print(json.dumps(results))
|
||||
@@ -0,0 +1,45 @@
|
||||
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier: GPL-3.0-or-later */
|
||||
#ifdef NDEBUG
|
||||
#error Timing checks require assertions
|
||||
#endif
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "../../../record/record_aac_stream_timing.h"
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
static const unsigned rates[] = {8000,32000,44100,48000,96000,192000};
|
||||
record_aac_stream_timing_t t, zero;
|
||||
unsigned rate, delay;
|
||||
(void)argv;
|
||||
memset(&zero, 0, sizeof(zero));
|
||||
assert(!record_aac_stream_timing(48000, 1024, NULL));
|
||||
memset(&t, 255, sizeof(t));
|
||||
assert(!record_aac_stream_timing(0, 1024, &t));
|
||||
assert(!memcmp(&t, &zero, sizeof(t)));
|
||||
assert(!record_aac_stream_timing(192001, 1024, &t));
|
||||
assert(!record_aac_stream_timing(48000, 4097, &t));
|
||||
for (rate = 0; rate < sizeof(rates) / sizeof(rates[0]); rate++)
|
||||
for (delay = 0; delay <= 4096; delay++)
|
||||
{
|
||||
uint64_t video, audio;
|
||||
assert(record_aac_stream_timing(rates[rate], delay, &t));
|
||||
assert(t.input_epoch >= RECORD_AAC_PROBE_FRAMES + RECORD_AAC_STREAM_GUARD
|
||||
+ RECORD_AAC_STREAM_PREROLL);
|
||||
assert(t.input_epoch < RECORD_AAC_PROBE_FRAMES + RECORD_AAC_STREAM_GUARD
|
||||
+ RECORD_AAC_STREAM_PREROLL + 1024);
|
||||
assert((t.input_epoch + delay) % 1024 == 0);
|
||||
assert(t.coded_origin % 1024 == 0);
|
||||
assert(t.input_epoch + delay - t.coded_origin == t.audio_epoch);
|
||||
assert(t.audio_epoch == 2048);
|
||||
video = (uint64_t)t.video_epoch_us * rates[rate];
|
||||
audio = (uint64_t)t.audio_epoch * 1000000;
|
||||
assert(video >= audio && video - audio < rates[rate]);
|
||||
if (argc > 1 && delay == 1024 && (rates[rate] == 44100 || rates[rate] == 48000))
|
||||
printf("%u %u %u %u %u\n", rates[rate], t.input_epoch,
|
||||
t.coded_origin, t.audio_epoch, t.video_epoch_us);
|
||||
}
|
||||
if (argc == 1)
|
||||
puts("PASS: all 4097 calibrated AAC delays at six rates, source boundary alignment and sub-microsecond video epoch rounding");
|
||||
return 0;
|
||||
}
|
||||
@@ -21,6 +21,8 @@ static const char *test_mime = "video/hevc";
|
||||
static unsigned hdr_mode, format_fault, normalized_profile;
|
||||
static unsigned char hdr_saved[25];
|
||||
static uint8_t payload[16];
|
||||
static uint8_t video_csd[2][12];
|
||||
static unsigned csd_fault;
|
||||
static int allocation_fail;
|
||||
static void *test_allocate(size_t count, size_t bytes)
|
||||
{
|
||||
@@ -182,8 +184,15 @@ static bool get_integer(AMediaFormat *f, const char *key, int32_t *value)
|
||||
static bool get_buffer(
|
||||
AMediaFormat *f, const char *key, void **data, size_t *size)
|
||||
{
|
||||
assert((f == &output_formats[0] || f == &output_formats[1]) && hdr_mode
|
||||
&& !strcmp(key, "hdr-static-info"));
|
||||
assert(f == &output_formats[0] || f == &output_formats[1]);
|
||||
if (!strcmp(key, "csd-0") || !strcmp(key, "csd-1"))
|
||||
{
|
||||
unsigned index = key[4] == '1' ? 1 : 0;
|
||||
*data = csd_fault == 2 ? NULL : video_csd[index];
|
||||
*size = csd_fault == 3 ? 0 : csd_fault == 4 ? 65536 : sizeof(video_csd[index]);
|
||||
return csd_fault != 1;
|
||||
}
|
||||
assert(hdr_mode && !strcmp(key, "hdr-static-info"));
|
||||
*data = format_fault == 15 ? NULL : hdr_saved;
|
||||
*size = format_fault == 14 ? 24 : 25;
|
||||
return format_fault != 13;
|
||||
@@ -339,6 +348,8 @@ int main(void)
|
||||
record_mediacodec_encoder_t *s;
|
||||
record_mediacodec_packet_t packet, again;
|
||||
int i, before;
|
||||
const void *csd;
|
||||
size_t csd_bytes;
|
||||
for (missing = 1; missing <= 22; missing++)
|
||||
{
|
||||
assert(!open_encoder());
|
||||
@@ -359,7 +370,26 @@ int main(void)
|
||||
assert(s && record_mediacodec_encoder_surface(s) == &window);
|
||||
output_mode = 0;
|
||||
assert(record_mediacodec_encoder_pump(s, &packet) == RECORD_MC_BUSY);
|
||||
assert(!record_mediacodec_encoder_video_config(s, 0, &csd, &csd_bytes)
|
||||
&& !csd && !csd_bytes);
|
||||
format_ready(s);
|
||||
before = output_calls;
|
||||
for (i = 0; i < 2; i++)
|
||||
{
|
||||
assert(record_mediacodec_encoder_video_config(s, (unsigned)i, &csd, &csd_bytes));
|
||||
assert(csd == video_csd[i] && csd_bytes == sizeof(video_csd[i]));
|
||||
}
|
||||
for (csd_fault = 1; csd_fault <= 4; csd_fault++)
|
||||
assert(!record_mediacodec_encoder_video_config(s, 0, &csd, &csd_bytes)
|
||||
&& !csd && !csd_bytes);
|
||||
csd_fault = 0;
|
||||
assert(!record_mediacodec_encoder_video_config(s, 2, &csd, &csd_bytes)
|
||||
&& !csd && !csd_bytes);
|
||||
assert(!record_mediacodec_encoder_video_config(s, 0, NULL, &csd_bytes)
|
||||
&& !csd_bytes);
|
||||
assert(!record_mediacodec_encoder_video_config(s, 0, &csd, NULL) && !csd);
|
||||
assert(output_calls == before && !owned);
|
||||
assert(record_mediacodec_encoder_video_config(s, 0, &csd, &csd_bytes));
|
||||
format_ready(s);
|
||||
output_mode = -2;
|
||||
assert(record_mediacodec_encoder_pump(s, &packet) == RECORD_MC_PROGRESS);
|
||||
@@ -516,6 +546,8 @@ int main(void)
|
||||
before = stops;
|
||||
stop_fault = i == 1;
|
||||
assert(record_mediacodec_encoder_stop(s) == !stop_fault);
|
||||
assert(!record_mediacodec_encoder_video_config(s, 0, &csd,
|
||||
&csd_bytes) && !csd && !csd_bytes);
|
||||
assert(stops == before + 1 && codecs == 1 && windows == 1
|
||||
&& libraries == 2 && formats == 2);
|
||||
assert(record_mediacodec_encoder_stop(s) == !stop_fault);
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier: GPL-3.0-or-later */
|
||||
#ifndef RECORD_WORKER_OUTPUT_FIXTURE_H
|
||||
#define RECORD_WORKER_OUTPUT_FIXTURE_H
|
||||
struct record_busy_output
|
||||
{
|
||||
record_mux_mediacodec_t *mux;
|
||||
record_mediacodec_packet_t saved[2];
|
||||
const record_mediacodec_packet_t *borrow[2];
|
||||
unsigned busy[2], accepted[2], retire_busy, retires;
|
||||
};
|
||||
static bool busy_track(void *data, bool audio, void *format)
|
||||
{
|
||||
struct record_busy_output *o = (struct record_busy_output*)data;
|
||||
return record_mux_mediacodec_track(o->mux, audio, format);
|
||||
}
|
||||
static int busy_start(void *data)
|
||||
{
|
||||
return record_mux_mediacodec_start(((struct record_busy_output*)data)->mux);
|
||||
}
|
||||
static int busy_write(void *data, bool audio, const record_mediacodec_packet_t *packet)
|
||||
{
|
||||
struct record_busy_output *o = (struct record_busy_output*)data;
|
||||
unsigned at = audio ? 1 : 0;
|
||||
if (o->borrow[at])
|
||||
{
|
||||
assert(o->borrow[at] == packet && o->saved[at].data == packet->data);
|
||||
assert(o->saved[at].size == packet->size && o->saved[at].pts_us == packet->pts_us);
|
||||
assert(o->saved[at].flags == packet->flags);
|
||||
}
|
||||
if (o->busy[at])
|
||||
{
|
||||
o->borrow[at] = packet;
|
||||
o->saved[at] = *packet;
|
||||
o->busy[at]--;
|
||||
return 0;
|
||||
}
|
||||
if (!record_mux_mediacodec_write(o->mux, audio, packet))
|
||||
return -1;
|
||||
o->borrow[at] = NULL;
|
||||
o->accepted[at]++;
|
||||
return 1;
|
||||
}
|
||||
static bool busy_end(void *data, bool audio, int64_t end_us)
|
||||
{
|
||||
return record_mux_mediacodec_end(((struct record_busy_output*)data)->mux, audio, end_us);
|
||||
}
|
||||
static int busy_finish(void *data)
|
||||
{
|
||||
return record_mux_mediacodec_finish(((struct record_busy_output*)data)->mux);
|
||||
}
|
||||
static bool busy_retire(void *data)
|
||||
{
|
||||
struct record_busy_output *o = (struct record_busy_output*)data;
|
||||
o->retires++;
|
||||
if (o->retire_busy)
|
||||
{
|
||||
o->retire_busy--;
|
||||
return false;
|
||||
}
|
||||
o->borrow[0] = o->borrow[1] = NULL;
|
||||
return true;
|
||||
}
|
||||
static const record_mediacodec_output_ops_t busy_ops = {
|
||||
busy_track, busy_start, busy_write, busy_end, busy_finish, busy_retire
|
||||
};
|
||||
#endif
|
||||
@@ -165,6 +165,8 @@ int record_mux_mediacodec_finish(record_mux_mediacodec_t *m)
|
||||
return 1;
|
||||
}
|
||||
|
||||
#include "worker_output_fixture.h"
|
||||
|
||||
static void exercise(unsigned video_delay, unsigned audio_delay,
|
||||
unsigned channels, unsigned failure, bool late_close)
|
||||
{
|
||||
@@ -237,6 +239,61 @@ static void exercise(unsigned video_delay, unsigned audio_delay,
|
||||
}
|
||||
}
|
||||
|
||||
static void output_backpressure(bool cancel)
|
||||
{
|
||||
record_mediacodec_encoder_t video;
|
||||
record_mux_mediacodec_t mux;
|
||||
struct record_busy_output output;
|
||||
record_mediacodec_output_ops_t ops = busy_ops;
|
||||
record_mediacodec_worker_t *w;
|
||||
unsigned i, pumps;
|
||||
int result = 0;
|
||||
memset(&video, 0, sizeof(video));
|
||||
memset(&mux, 0, sizeof(mux));
|
||||
memset(&output, 0, sizeof(output));
|
||||
fault = 0;
|
||||
output.mux = &mux;
|
||||
output.busy[0] = cancel ? 20 : 4;
|
||||
output.retire_busy = 2;
|
||||
w = record_mediacodec_worker_new_output(&video, NULL, NULL, &ops, &output, 0, 0);
|
||||
assert(w);
|
||||
memset(&ops, 0, sizeof(ops)); /* Worker owns its callback-table copy. */
|
||||
assert(record_mediacodec_worker_finish(w, 33333, 0));
|
||||
assert(record_mediacodec_worker_step(w) == RECORD_MC_WORKER_PROGRESS);
|
||||
assert(record_mediacodec_worker_step(w) != RECORD_MC_WORKER_ERROR);
|
||||
assert(video.borrowed && !video.releases && output.borrow[0]);
|
||||
pumps = video.pumps;
|
||||
for (i = 0; i < 3; i++)
|
||||
{
|
||||
assert(record_mediacodec_worker_step(w) != RECORD_MC_WORKER_ERROR);
|
||||
assert(video.pumps == pumps && !video.releases && video.borrowed);
|
||||
}
|
||||
if (cancel)
|
||||
{
|
||||
record_mediacodec_worker_abort(w);
|
||||
assert(record_mediacodec_worker_step(w) == RECORD_MC_WORKER_ERROR);
|
||||
}
|
||||
else
|
||||
{
|
||||
for (i = 0; i < 32; i++)
|
||||
{
|
||||
result = record_mediacodec_worker_step(w);
|
||||
if (result == RECORD_MC_WORKER_END || result == RECORD_MC_WORKER_ERROR)
|
||||
break;
|
||||
}
|
||||
assert(result == RECORD_MC_WORKER_END && video.releases == 2);
|
||||
assert(output.accepted[0] == 2 && !output.borrow[0]);
|
||||
}
|
||||
assert(!record_mediacodec_worker_free(w));
|
||||
if (cancel)
|
||||
assert(output.borrow[0]->data == &video.data && video.borrowed && !video.releases);
|
||||
assert(!record_mediacodec_worker_free(w));
|
||||
assert(record_mediacodec_worker_free(w) && output.retires == 3);
|
||||
assert(!output.borrow[0]);
|
||||
puts(cancel ? "PASS worker abort retains output borrow through delayed retirement"
|
||||
: "PASS worker output BUSY retains immutable packet without repump or early release");
|
||||
}
|
||||
|
||||
static void contracts(void)
|
||||
{
|
||||
record_mediacodec_encoder_t video, audio;
|
||||
@@ -292,6 +349,8 @@ int main(void)
|
||||
for (failure = 1; failure <= 13; failure++)
|
||||
exercise(0, 4, 2, failure, false);
|
||||
contracts();
|
||||
output_backpressure(false);
|
||||
output_backpressure(true);
|
||||
assert(record_mediacodec_worker_free(NULL));
|
||||
record_mediacodec_worker_abort(NULL);
|
||||
puts("PASS: Android recording worker PCM backpressure, mux startup and drain");
|
||||
|
||||
@@ -26,6 +26,3 @@
|
||||
|
||||
/session_gpu_refusal_test
|
||||
/session_gpu_refusal_test.exe
|
||||
|
||||
/hardware_id_test
|
||||
/hardware_id_test.exe
|
||||
@@ -9,7 +9,7 @@ ifneq ($(SANITIZER),)
|
||||
CFLAGS += -fsanitize=$(SANITIZER) -fno-omit-frame-pointer
|
||||
LDFLAGS += -fsanitize=$(SANITIZER)
|
||||
endif
|
||||
all: hardware_id_test$(EXE) av_output_test$(EXE) audio_lend_test$(EXE) packet_test$(EXE) session_gpu_refusal_test$(EXE) av_session_test$(EXE) crop_test$(EXE) av_worker_test$(EXE) audio_queue_test$(EXE) slots_test$(EXE) session_test$(EXE) worker_test$(EXE) audio_rate_test$(EXE) audio_rate_native_test$(EXE)
|
||||
all: av_output_test$(EXE) audio_lend_test$(EXE) packet_test$(EXE) session_gpu_refusal_test$(EXE) av_session_test$(EXE) crop_test$(EXE) av_worker_test$(EXE) audio_queue_test$(EXE) slots_test$(EXE) session_test$(EXE) worker_test$(EXE) audio_rate_test$(EXE) audio_rate_native_test$(EXE)
|
||||
av_output_test$(EXE): av_output_test.c av_worker_test.c $(RA)/record/record_avfoundation_session.c $(RA)/record/record_avfoundation_session.h
|
||||
$(CC) $(CFLAGS) -Wno-unused-function -o $@ av_output_test.c $(RA)/record/record_avfoundation_session.c $(RA)/record/record_avfoundation_worker.c $(RA)/record/record_gpu_worker_vt.c $(RA)/record/record_gpu_queue.c $(RA)/record/record_audio_queue.c $(RA)/libretro-common/queues/retro_spsc.c $(LDFLAGS)
|
||||
audio_lend_test$(EXE): audio_lend_test.c $(RA)/record/record_audio_queue.c $(RA)/record/record_audio_queue.h
|
||||
@@ -34,11 +34,9 @@ crop_test$(EXE): crop_test.c $(RA)/libretro-common/formats/h265/rh265_sps_crop.c
|
||||
$(CC) $(CFLAGS) -o $@ $< $(LDFLAGS)
|
||||
worker_test$(EXE): worker_test.c $(RA)/record/record_gpu_worker_vt.c $(RA)/record/record_gpu_worker_vt.h $(RA)/record/record_gpu_queue.c
|
||||
$(CC) $(CFLAGS) -o $@ worker_test.c $(RA)/record/record_gpu_worker_vt.c $(RA)/record/record_gpu_queue.c $(RA)/libretro-common/queues/retro_spsc.c $(LDFLAGS)
|
||||
session_gpu_refusal_test$(EXE): session_test.c $(RA)/gfx/common/videotoolbox_encode.h $(RA)/gfx/common/videotoolbox_encode_hardware.h
|
||||
session_gpu_refusal_test$(EXE): session_test.c $(RA)/gfx/common/videotoolbox_encode.h
|
||||
$(CC) $(CFLAGS) -DVIDEOTOOLBOX_ENCODE_TEST_NO_GPU_SELECTION -o $@ $< $(LDFLAGS)
|
||||
session_test$(EXE): session_test.c $(RA)/gfx/common/videotoolbox_encode.h $(RA)/gfx/common/videotoolbox_encode_slots.h $(RA)/gfx/common/videotoolbox_encode_hardware.h
|
||||
$(CC) $(CFLAGS) -o $@ $< $(LDFLAGS)
|
||||
hardware_id_test$(EXE): hardware_id_test.c $(RA)/gfx/common/videotoolbox_encode_hardware.h
|
||||
session_test$(EXE): session_test.c $(RA)/gfx/common/videotoolbox_encode.h $(RA)/gfx/common/videotoolbox_encode_slots.h
|
||||
$(CC) $(CFLAGS) -o $@ $< $(LDFLAGS)
|
||||
slots_test$(EXE): slots_test.c $(RA)/gfx/common/videotoolbox_encode_slots.h
|
||||
$(CC) $(CFLAGS) -o $@ $< $(LDFLAGS)
|
||||
@@ -60,7 +58,6 @@ check: all legacy-check
|
||||
$(RUNNER) ./worker_test$(EXE)
|
||||
$(RUNNER) ./audio_rate_test$(EXE)
|
||||
clean:
|
||||
rm -f hardware_id_test hardware_id_test.exe
|
||||
rm -f av_output_test av_output_test.exe
|
||||
rm -f audio_lend_test audio_lend_test.exe
|
||||
rm -f packet_test packet_test.exe
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier: GPL-3.0-or-later */
|
||||
#include <assert.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <boolean.h>
|
||||
#include <retro_inline.h>
|
||||
|
||||
typedef struct object object_t;
|
||||
typedef object_t *CFTypeRef;
|
||||
typedef object_t *CFArrayRef;
|
||||
typedef object_t *CFDictionaryRef;
|
||||
typedef object_t *CFStringRef;
|
||||
typedef object_t *CFNumberRef;
|
||||
typedef object_t *CFBooleanRef;
|
||||
typedef long CFIndex;
|
||||
typedef int OSStatus;
|
||||
struct object
|
||||
{
|
||||
unsigned type, refs;
|
||||
int64_t number;
|
||||
const char *text;
|
||||
object_t *values[3];
|
||||
};
|
||||
#define kVTVideoEncoderList_IsHardwareAccelerated 0
|
||||
#define kVTVideoEncoderList_CodecType 1
|
||||
#define kVTVideoEncoderList_EncoderID 2
|
||||
#define kCFNumberSInt64Type 1
|
||||
static object_t list, entry, hardware, codec, identifier, requested;
|
||||
static CFIndex count = 1;
|
||||
static OSStatus list_status;
|
||||
static unsigned visits;
|
||||
static CFTypeRef CFRetain(CFTypeRef object)
|
||||
{
|
||||
assert(object && object->refs);
|
||||
object->refs++;
|
||||
return object;
|
||||
}
|
||||
static void CFRelease(CFTypeRef object)
|
||||
{
|
||||
assert(object && object->refs > 1);
|
||||
object->refs--;
|
||||
}
|
||||
static OSStatus VTCopyVideoEncoderList(void *options, CFArrayRef *output)
|
||||
{
|
||||
(void)options;
|
||||
*output = CFRetain(&list);
|
||||
return list_status;
|
||||
}
|
||||
static CFIndex CFArrayGetCount(CFArrayRef array)
|
||||
{
|
||||
assert(array == &list);
|
||||
return count;
|
||||
}
|
||||
static CFTypeRef CFArrayGetValueAtIndex(CFArrayRef array, CFIndex index)
|
||||
{
|
||||
assert(array == &list && index == 0);
|
||||
visits++;
|
||||
return &entry;
|
||||
}
|
||||
static unsigned CFGetTypeID(CFTypeRef object)
|
||||
{
|
||||
return object->type;
|
||||
}
|
||||
static unsigned CFDictionaryGetTypeID(void)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
static unsigned CFBooleanGetTypeID(void)
|
||||
{
|
||||
return 2;
|
||||
}
|
||||
static unsigned CFNumberGetTypeID(void)
|
||||
{
|
||||
return 3;
|
||||
}
|
||||
static unsigned CFStringGetTypeID(void)
|
||||
{
|
||||
return 4;
|
||||
}
|
||||
static CFTypeRef CFDictionaryGetValue(CFDictionaryRef object, unsigned key)
|
||||
{
|
||||
assert(object == &entry && key < 3);
|
||||
return object->values[key];
|
||||
}
|
||||
static bool CFBooleanGetValue(CFBooleanRef value)
|
||||
{
|
||||
return value->number != 0;
|
||||
}
|
||||
static bool CFNumberGetValue(CFNumberRef value, unsigned type, void *output)
|
||||
{
|
||||
assert(type == kCFNumberSInt64Type);
|
||||
*(int64_t*)output = value->number;
|
||||
return true;
|
||||
}
|
||||
static unsigned CFStringGetLength(CFStringRef value)
|
||||
{
|
||||
return (unsigned)strlen(value->text);
|
||||
}
|
||||
static bool CFEqual(CFTypeRef first, CFStringRef second)
|
||||
{
|
||||
return !strcmp(first->text, second->text);
|
||||
}
|
||||
#define VIDEOTOOLBOX_ENCODE_HARDWARE_LIST_TEST
|
||||
#include "../../../gfx/common/videotoolbox_encode_hardware.h"
|
||||
|
||||
int main(void)
|
||||
{
|
||||
CFStringRef selected;
|
||||
list.refs = entry.refs = hardware.refs = codec.refs = identifier.refs = requested.refs = 1;
|
||||
entry.type = 1; hardware.type = 2; codec.type = 3; identifier.type = requested.type = 4;
|
||||
hardware.number = 1; codec.number = 7;
|
||||
identifier.text = requested.text = "hardware-encoder";
|
||||
entry.values[0] = &hardware;
|
||||
entry.values[1] = &codec;
|
||||
entry.values[2] = &identifier;
|
||||
selected = videotoolbox_encode_hardware_id(NULL, 7);
|
||||
assert(selected == &identifier && identifier.refs == 2 && list.refs == 1);
|
||||
CFRelease(selected);
|
||||
selected = videotoolbox_encode_hardware_id(&requested, 7);
|
||||
assert(selected == &identifier);
|
||||
CFRelease(selected);
|
||||
requested.text = "other-encoder";
|
||||
assert(!videotoolbox_encode_hardware_id(&requested, 7));
|
||||
assert(!videotoolbox_encode_hardware_id(NULL, 8));
|
||||
hardware.number = 0;
|
||||
assert(!videotoolbox_encode_hardware_id(NULL, 7));
|
||||
hardware.number = 1;
|
||||
hardware.type = 3;
|
||||
assert(!videotoolbox_encode_hardware_id(NULL, 7));
|
||||
hardware.type = 2;
|
||||
identifier.text = "";
|
||||
assert(!videotoolbox_encode_hardware_id(NULL, 7));
|
||||
identifier.text = "hardware-encoder";
|
||||
entry.values[2] = NULL;
|
||||
assert(!videotoolbox_encode_hardware_id(NULL, 7));
|
||||
entry.values[2] = &identifier;
|
||||
list_status = -1;
|
||||
assert(!videotoolbox_encode_hardware_id(NULL, 7) && list.refs == 1);
|
||||
list_status = 0;
|
||||
visits = 0;
|
||||
count = 4097;
|
||||
assert(!videotoolbox_encode_hardware_id(NULL, 7) && !visits);
|
||||
count = -1;
|
||||
assert(!videotoolbox_encode_hardware_id(NULL, 7) && !visits);
|
||||
assert(identifier.refs == 1 && list.refs == 1);
|
||||
puts("PASS advertised hardware encoder ID, codec admission, malformed lists and retained ownership");
|
||||
return 0;
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build and link the production iOS application; never report device acceptance."""
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--output-dir', type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
if sys.platform != 'darwin':
|
||||
parser.error('full iOS application linking requires macOS/Xcode')
|
||||
root = Path(__file__).resolve().parents[3]
|
||||
output = args.output_dir.resolve()
|
||||
output.mkdir(parents=True, exist_ok=True)
|
||||
common = ['xcodebuild', '-project', 'pkg/apple/RetroArch_iOS13.xcodeproj',
|
||||
'-scheme', 'RetroArch iOS Release', '-configuration', 'Release',
|
||||
'-destination', 'generic/platform=iOS', '-xcconfig', 'pkg/apple/GitHubCI.xcconfig',
|
||||
'-derivedDataPath', str(output / 'derived'),
|
||||
'CODE_SIGNING_ALLOWED=NO', 'CODE_SIGNING_REQUIRED=NO',
|
||||
'CODE_SIGN_IDENTITY=', 'PROVISIONING_PROFILE_SPECIFIER=']
|
||||
settings = subprocess.run(common + ['-showBuildSettings', '-json'], cwd=root,
|
||||
check=True, capture_output=True, text=True, timeout=180)
|
||||
(output / 'build-settings.json').write_text(settings.stdout)
|
||||
rows = json.loads(settings.stdout)
|
||||
main_target = next(row['buildSettings'] for row in rows if row['target'] == 'RetroArchiOS')
|
||||
flags = shlex.split(main_target['OTHER_CFLAGS'])
|
||||
defines = {flag[2:].split('=', 1)[0] for flag in flags if flag.startswith('-D')}
|
||||
required = {'HAVE_METAL', 'HAVE_AVF', 'HAVE_THREADS', 'HAVE_CONFIGFILE',
|
||||
'HAVE_NETWORKING', 'HAVE_CRYPTO', 'HAVE_RTMP', 'HAVE_RFLV'}
|
||||
assert required <= defines, sorted(required - defines)
|
||||
assert 'HAVE_FFMPEG' not in defines
|
||||
command = common + ['-resultBundlePath', str(output / 'build.xcresult'), 'build']
|
||||
report = dict(command=command, required_defines=sorted(required),
|
||||
scope='Full unsigned device application build/link only; no physical playback or recording acceptance',
|
||||
result='FAIL')
|
||||
(output / 'results.json').write_text(json.dumps(report, indent=2) + '\n')
|
||||
with (output / 'xcodebuild.log').open('w') as log:
|
||||
result = subprocess.run(command, cwd=root, stdout=log, stderr=subprocess.STDOUT, timeout=3000)
|
||||
report['exit_code'] = result.returncode
|
||||
if not result.returncode:
|
||||
executable = output / 'derived/Build/Products/Release-iphoneos/RetroArch.app/RetroArch'
|
||||
assert executable.is_file() and executable.stat().st_size > 0
|
||||
linked = subprocess.run(['otool', '-L', str(executable)], check=True,
|
||||
capture_output=True, text=True, timeout=60).stdout
|
||||
(output / 'linked-libraries.txt').write_text(linked)
|
||||
assert not any(name in linked.lower() for name in ('libavcodec', 'libavformat', 'libavutil', 'libswscale'))
|
||||
report.update(result='PASS', executable_bytes=executable.stat().st_size)
|
||||
(output / 'results.json').write_text(json.dumps(report, indent=2) + '\n')
|
||||
print(report['result'], report['scope'], flush=True)
|
||||
return int(bool(result.returncode))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
@@ -1,67 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compile production VT headers against real iOS SDKs; no device acceptance claim."""
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
|
||||
def check(sdk, minimum, output):
|
||||
root = Path(__file__).resolve().parents[3]
|
||||
sdk_path = subprocess.check_output(['xcrun', '--sdk', sdk, '--show-sdk-path'], text=True).strip()
|
||||
clang = subprocess.check_output(['xcrun', '--sdk', sdk, '--find', 'clang'], text=True).strip()
|
||||
target = 'arm64-apple-ios' + minimum + ('-simulator' if sdk == 'iphonesimulator' else '')
|
||||
with tempfile.TemporaryDirectory(prefix='vt-ios-sdk-') as tmp:
|
||||
probe = Path(tmp) / 'probe.m'
|
||||
probe.write_text('#include "gfx/common/videotoolbox_encode.h"\n'
|
||||
'#include "record/drivers/record_metal.h"\n'
|
||||
'#ifndef HAVE_RECORD_METAL\n#error native frontend unavailable\n#endif\n'
|
||||
'#ifndef RECORD_METAL_NATIVE_SDK\n#error mobile recording compiled as a stub\n#endif\n'
|
||||
'#ifndef VIDEOTOOLBOX_ENCODE_SLOTS\n#error native encoding slots unavailable\n#endif\n'
|
||||
'VT_ENCODE_AVAILABLE void *record_vt_ios_sdk_probe(\n'
|
||||
' const videotoolbox_encode_config_t *config, OSStatus *status)\n'
|
||||
'{\n return videotoolbox_encode_new(config, status);\n}\n')
|
||||
command = [clang, '-target', target, '-isysroot', sdk_path, '-std=gnu89',
|
||||
'-DHAVE_THREADS', '-DHAVE_METAL', '-DHAVE_AVF', '-DHAVE_CONFIGFILE', '-DHAVE_NETWORKING',
|
||||
'-DHAVE_RTMP', '-DHAVE_RFLV', '-DHAVE_CRYPTO', '-fblocks', '-fobjc-arc', '-I' + str(root), '-I' + str(root / 'libretro-common/include'),
|
||||
'-Wall', '-Wextra', '-Werror', '-Wunguarded-availability-new',
|
||||
'-Wno-unused-function', '-fsyntax-only', str(probe)]
|
||||
units = [probe] + [root / ('record/' + name) for name in (
|
||||
'record_gpu_capture_metal.m', 'record_gpu_session_metal.m',
|
||||
'record_gpu_dispatch_metal.m', 'record_mux_avfoundation.m', 'drivers/record_metal.c')]
|
||||
results = []
|
||||
for unit in units:
|
||||
invocation = command[:-1] + [str(unit)]
|
||||
if unit.suffix == '.c':
|
||||
invocation.remove('-fobjc-arc')
|
||||
result = subprocess.run(invocation, text=True, capture_output=True, timeout=120)
|
||||
name = sdk + '-' + minimum + '-' + unit.stem
|
||||
(output / (name + '.log')).write_text(result.stdout + result.stderr)
|
||||
results.append(dict(sdk=sdk, minimum=minimum, target=target, unit=unit.name,
|
||||
command=invocation, result='PASS' if not result.returncode else 'FAIL',
|
||||
exit_code=result.returncode))
|
||||
return results
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--output-dir', type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
if sys.platform != 'darwin':
|
||||
parser.error('real Apple SDK compilation requires macOS/Xcode; no local pass is reported')
|
||||
args.output_dir.mkdir(parents=True, exist_ok=True)
|
||||
results = [case for sdk in ('iphoneos', 'iphonesimulator')
|
||||
for minimum in ('13.0', '17.4')
|
||||
for case in check(sdk, minimum, args.output_dir)]
|
||||
(args.output_dir / 'results.json').write_text(json.dumps(dict(
|
||||
scope='SDK production unit compilation only; no full application link or device acceptance',
|
||||
cases=results), indent=2) + '\n')
|
||||
for result in results:
|
||||
print(result['result'], result['target'], result['unit'], flush=True)
|
||||
return int(any(result['exit_code'] for result in results))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
Reference in new issue
Block a user