You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
575 lines
16 KiB
575 lines
16 KiB
/*****************************************************************************
|
|
* avsamplebuffer.m: AVSampleBufferRender plugin for iOS and macOS
|
|
*****************************************************************************
|
|
* Copyright (C) 2024 VLC authors, VideoLAN and VideoLABS
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify it
|
|
* under the terms of the GNU Lesser General Public License as published by
|
|
* the Free Software Foundation; either version 2.1 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public License
|
|
* along with this program; if not, write to the Free Software Foundation,
|
|
* Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
|
|
*****************************************************************************/
|
|
|
|
#import "config.h"
|
|
|
|
#import <TargetConditionals.h>
|
|
#import <Foundation/Foundation.h>
|
|
#import <AVFoundation/AVFoundation.h>
|
|
|
|
#import <vlc_common.h>
|
|
#import <vlc_plugin.h>
|
|
#import <vlc_aout.h>
|
|
|
|
#if TARGET_OS_IPHONE || TARGET_OS_TV || TARGET_OS_VISION
|
|
#define HAS_AVAUDIOSESSION
|
|
#import "avaudiosession_common.h"
|
|
#endif
|
|
|
|
#import "channel_layout.h"
|
|
|
|
// for (void)setRate:(float)rate time:(CMTime)time atHostTime:(CMTime)hostTime
|
|
#define MIN_MACOS 11.3
|
|
#define MIN_IOS 14.5
|
|
#define MIN_TVOS 14.5
|
|
|
|
// work-around to fix compilation on older Xcode releases
|
|
#if defined(TARGET_OS_VISION) && TARGET_OS_VISION
|
|
#define MIN_VISIONOS 1.0
|
|
#define VISIONOS_API_AVAILABLE , visionos(MIN_VISIONOS)
|
|
#define VISIONOS_AVAILABLE , visionOS MIN_VISIONOS
|
|
#else
|
|
#define VISIONOS_API_AVAILABLE
|
|
#define VISIONOS_AVAILABLE
|
|
#endif
|
|
|
|
#pragma mark Private
|
|
|
|
API_AVAILABLE(macos(MIN_MACOS), ios(MIN_IOS), tvos(MIN_TVOS) VISIONOS_API_AVAILABLE)
|
|
@interface VLCAVSample : NSObject
|
|
{
|
|
audio_output_t *_aout;
|
|
|
|
CMAudioFormatDescriptionRef _fmtDesc;
|
|
AVSampleBufferAudioRenderer *_renderer;
|
|
AVSampleBufferRenderSynchronizer *_sync;
|
|
id _observer;
|
|
dispatch_queue_t _dataQueue;
|
|
dispatch_queue_t _timeQueue;
|
|
size_t _bytesPerFrame;
|
|
|
|
vlc_mutex_t _bufferLock;
|
|
vlc_cond_t _bufferWait;
|
|
|
|
block_t *_outChain;
|
|
block_t **_outChainLast;
|
|
|
|
int64_t _ptsSamples;
|
|
vlc_tick_t _firstPts;
|
|
unsigned _sampleRate;
|
|
BOOL _stopped;
|
|
}
|
|
@end
|
|
|
|
@implementation VLCAVSample
|
|
|
|
- (id)init:(audio_output_t*)aout
|
|
{
|
|
_aout = aout;
|
|
_dataQueue = dispatch_queue_create("VLC AVSampleBuffer data queue", DISPATCH_QUEUE_SERIAL);
|
|
if (_dataQueue == nil)
|
|
return nil;
|
|
|
|
_timeQueue = dispatch_queue_create("VLC AVSampleBuffer time queue", DISPATCH_QUEUE_SERIAL);
|
|
if (_timeQueue == nil)
|
|
return nil;
|
|
|
|
vlc_mutex_init(&_bufferLock);
|
|
vlc_cond_init(&_bufferWait);
|
|
|
|
_outChain = NULL;
|
|
_outChainLast = &_outChain;
|
|
|
|
self = [super init];
|
|
if (self == nil)
|
|
return nil;
|
|
|
|
return self;
|
|
}
|
|
|
|
- (void)dealloc
|
|
{
|
|
block_ChainRelease(_outChain);
|
|
}
|
|
|
|
- (void)clearOutChain
|
|
{
|
|
block_ChainRelease(_outChain);
|
|
_outChain = NULL;
|
|
_outChainLast = &_outChain;
|
|
}
|
|
|
|
static void
|
|
customBlock_Free(void *refcon, void *doomedMemoryBlock, size_t sizeInBytes)
|
|
{
|
|
block_t *block = refcon;
|
|
|
|
assert(block->i_buffer == sizeInBytes);
|
|
block_Release(block);
|
|
|
|
(void) doomedMemoryBlock;
|
|
(void) sizeInBytes;
|
|
}
|
|
|
|
- (CMSampleBufferRef)wrapBuffer:(block_t **)pblock
|
|
{
|
|
// This function take the block ownership
|
|
block_t *block = *pblock;
|
|
*pblock = NULL;
|
|
|
|
const CMBlockBufferCustomBlockSource blockSource = {
|
|
.version = kCMBlockBufferCustomBlockSourceVersion,
|
|
.FreeBlock = customBlock_Free,
|
|
.refCon = block,
|
|
};
|
|
|
|
OSStatus status;
|
|
CMBlockBufferRef blockBuf;
|
|
status = CMBlockBufferCreateWithMemoryBlock(kCFAllocatorDefault,
|
|
block->p_buffer, // memoryBlock
|
|
block->i_buffer, // blockLength
|
|
NULL, // blockAllocator
|
|
&blockSource, // customBlockSource
|
|
0, // offsetToData
|
|
block->i_buffer, // dataLength
|
|
0, // flags
|
|
&blockBuf);
|
|
if (status != noErr)
|
|
{
|
|
msg_Err(_aout, "CMBlockBufferRef creation failure %li", (long)status);
|
|
return nil;
|
|
}
|
|
|
|
const CMSampleTimingInfo timeInfo = {
|
|
.duration = kCMTimeInvalid,
|
|
.presentationTimeStamp = CMTimeMake(_ptsSamples, _sampleRate),
|
|
.decodeTimeStamp = kCMTimeInvalid,
|
|
};
|
|
|
|
CMSampleBufferRef sampleBuf;
|
|
status = CMSampleBufferCreateReady(kCFAllocatorDefault,
|
|
blockBuf, // dataBuffer
|
|
_fmtDesc, // formatDescription
|
|
block->i_nb_samples, // numSamples
|
|
1, // numSampleTimingEntries
|
|
&timeInfo, // sampleTimingArray
|
|
1, // numSampleSizeEntries
|
|
&_bytesPerFrame, // sampleSizeArray
|
|
&sampleBuf);
|
|
CFRelease(blockBuf);
|
|
|
|
if (status != noErr)
|
|
{
|
|
msg_Warn(_aout, "CMSampleBufferRef creation failure %li", (long)status);
|
|
return nil;
|
|
}
|
|
|
|
return sampleBuf;
|
|
}
|
|
|
|
- (void)selectDevice:(const char *)name
|
|
{
|
|
}
|
|
|
|
- (void)setMute:(BOOL)muted
|
|
{
|
|
_renderer.muted = muted;
|
|
aout_MuteReport(_aout, muted);
|
|
}
|
|
|
|
- (void)setVolume:(float)volume
|
|
{
|
|
_renderer.volume = volume * volume * volume;
|
|
aout_VolumeReport(_aout, volume);
|
|
}
|
|
|
|
+ (vlc_tick_t)CMTimeTotick:(CMTime) timestamp
|
|
{
|
|
CMTime scaled = CMTimeConvertScale(
|
|
timestamp, CLOCK_FREQ,
|
|
kCMTimeRoundingMethod_Default);
|
|
|
|
return scaled.value;
|
|
}
|
|
|
|
- (void)flush
|
|
{
|
|
if (_ptsSamples >= 0)
|
|
[self stopSyncRenderer];
|
|
|
|
_ptsSamples = -1;
|
|
_firstPts = VLC_TICK_INVALID;
|
|
}
|
|
|
|
- (void)pause:(BOOL)pause date:(vlc_tick_t)date
|
|
{
|
|
(void) date;
|
|
|
|
if (_ptsSamples >= 0)
|
|
_sync.rate = pause ? 0.0f : 1.0f;
|
|
}
|
|
|
|
- (void)whenTimeObserved:(CMTime) time
|
|
{
|
|
assert(_firstPts != VLC_TICK_INVALID);
|
|
|
|
if (time.value == 0)
|
|
return;
|
|
vlc_tick_t system_now = vlc_tick_now();
|
|
vlc_tick_t pos_ticks = [VLCAVSample CMTimeTotick:time] + _firstPts;
|
|
|
|
aout_TimingReport(_aout, system_now, pos_ticks);
|
|
}
|
|
|
|
- (void)whenDataReady
|
|
{
|
|
vlc_mutex_lock(&_bufferLock);
|
|
|
|
while (_renderer.readyForMoreMediaData)
|
|
{
|
|
while (!_stopped && _outChain == NULL)
|
|
vlc_cond_wait(&_bufferWait, &_bufferLock);
|
|
|
|
if (_stopped)
|
|
{
|
|
vlc_mutex_unlock(&_bufferLock);
|
|
return;
|
|
}
|
|
|
|
block_t *block = _outChain;
|
|
_outChain = _outChain->p_next;
|
|
if (_outChain == NULL)
|
|
_outChainLast = &_outChain;
|
|
|
|
CMSampleBufferRef buffer = [self wrapBuffer:&block];
|
|
_ptsSamples += CMSampleBufferGetNumSamples(buffer);
|
|
|
|
[_renderer enqueueSampleBuffer:buffer];
|
|
|
|
CFRelease(buffer);
|
|
}
|
|
|
|
vlc_mutex_unlock(&_bufferLock);
|
|
}
|
|
|
|
- (void)play:(block_t *)block date:(vlc_tick_t)date
|
|
{
|
|
vlc_mutex_lock(&_bufferLock);
|
|
|
|
if (_ptsSamples == -1)
|
|
{
|
|
__weak typeof(self) weakSelf = self;
|
|
[_renderer requestMediaDataWhenReadyOnQueue:_dataQueue usingBlock:^{
|
|
[weakSelf whenDataReady];
|
|
}];
|
|
|
|
_firstPts = block->i_pts;
|
|
const CMTime interval = CMTimeMake(CLOCK_FREQ, CLOCK_FREQ);
|
|
_observer = [_sync addPeriodicTimeObserverForInterval:interval
|
|
queue:_timeQueue
|
|
usingBlock:^ (CMTime time){
|
|
[weakSelf whenTimeObserved:time];
|
|
}];
|
|
|
|
_ptsSamples = 0;
|
|
vlc_tick_t delta = date - vlc_tick_now();
|
|
CMTime hostTime = CMTimeAdd(CMClockGetTime(CMClockGetHostTimeClock()),
|
|
CMTimeMake(delta, CLOCK_FREQ));
|
|
CMTime time = CMTimeMake(_ptsSamples, _sampleRate);
|
|
|
|
_sync.delaysRateChangeUntilHasSufficientMediaData = NO;
|
|
[_sync setRate:1.0f time:time atHostTime:hostTime];
|
|
}
|
|
|
|
block_ChainLastAppend(&_outChainLast, block);
|
|
|
|
vlc_cond_signal(&_bufferWait);
|
|
vlc_mutex_unlock(&_bufferLock);
|
|
}
|
|
|
|
- (void)stopSyncRenderer
|
|
{
|
|
_sync.rate = 0.0f;
|
|
|
|
[_sync removeTimeObserver:_observer];
|
|
[_renderer stopRequestingMediaData];
|
|
[_renderer flush];
|
|
|
|
[self clearOutChain];
|
|
}
|
|
|
|
- (void)stop
|
|
{
|
|
NSNotificationCenter *notifCenter = [NSNotificationCenter defaultCenter];
|
|
|
|
vlc_mutex_lock(&_bufferLock);
|
|
_stopped = YES;
|
|
vlc_cond_signal(&_bufferWait);
|
|
vlc_mutex_unlock(&_bufferLock);
|
|
|
|
if (_ptsSamples > 0)
|
|
[self stopSyncRenderer];
|
|
|
|
[_sync removeRenderer:_renderer atTime:kCMTimeInvalid completionHandler:nil];
|
|
|
|
#ifdef HAS_AVAUDIOSESSION
|
|
avas_SetActive(_aout, [AVAudioSession sharedInstance], false,
|
|
AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation);
|
|
#endif
|
|
|
|
CFRelease(_fmtDesc);
|
|
|
|
[notifCenter removeObserver:self];
|
|
}
|
|
|
|
- (void)flushedAutomatically:(NSNotification *)notification
|
|
{
|
|
msg_Warn(_aout, "flushedAutomatically");
|
|
aout_RestartRequest(_aout, false);
|
|
}
|
|
|
|
- (void)outputConfigurationChanged:(NSNotification *)notification
|
|
{
|
|
msg_Warn(_aout, "outputConfigurationChanged");
|
|
aout_RestartRequest(_aout, false);
|
|
}
|
|
|
|
- (BOOL)start:(audio_sample_format_t *)fmt
|
|
{
|
|
if (aout_BitsPerSample(fmt->i_format) == 0)
|
|
return NO; /* Can handle PT */
|
|
|
|
NSNotificationCenter *notifCenter = [NSNotificationCenter defaultCenter];
|
|
|
|
fmt->i_format = VLC_CODEC_FL32;
|
|
|
|
#ifdef HAS_AVAUDIOSESSION
|
|
AVAudioSession *instance = [AVAudioSession sharedInstance];
|
|
if (avas_SetActive(_aout, instance, true, 0) != VLC_SUCCESS)
|
|
return NO;
|
|
avas_PrepareFormat(_aout, instance, fmt, true);
|
|
|
|
enum port_type port_type;
|
|
if (avas_GetPortType(_aout, instance, &port_type) == VLC_SUCCESS)
|
|
{
|
|
msg_Dbg(_aout, "Output on %s, channel count: %u",
|
|
port_type == PORT_TYPE_HDMI ? "HDMI" :
|
|
port_type == PORT_TYPE_USB ? "USB" :
|
|
port_type == PORT_TYPE_HEADPHONES ? "Headphones" : "Default",
|
|
aout_FormatNbChannels(fmt));
|
|
|
|
_aout->current_sink_info.headphones = port_type == PORT_TYPE_HEADPHONES;
|
|
}
|
|
#endif
|
|
|
|
AudioChannelLayout *inlayout_buf = NULL;
|
|
size_t inlayout_size = 0;
|
|
int err = channel_layout_MapFromVLC(_aout, fmt, &inlayout_buf,
|
|
&inlayout_size);
|
|
if (err != VLC_SUCCESS)
|
|
goto error_avas;
|
|
|
|
AudioStreamBasicDescription desc = {
|
|
.mSampleRate = fmt->i_rate,
|
|
.mFormatID = kAudioFormatLinearPCM,
|
|
.mFormatFlags = kAudioFormatFlagsNativeFloatPacked,
|
|
.mChannelsPerFrame = aout_FormatNbChannels(fmt),
|
|
.mFramesPerPacket = 1,
|
|
.mBitsPerChannel = 32,
|
|
};
|
|
|
|
desc.mBytesPerFrame = desc.mBitsPerChannel * desc.mChannelsPerFrame / 8;
|
|
desc.mBytesPerPacket = desc.mBytesPerFrame * desc.mFramesPerPacket;
|
|
|
|
OSStatus status =
|
|
CMAudioFormatDescriptionCreate(kCFAllocatorDefault,
|
|
&desc,
|
|
inlayout_size,
|
|
inlayout_buf,
|
|
0,
|
|
nil,
|
|
nil,
|
|
&_fmtDesc);
|
|
free(inlayout_buf);
|
|
if (status != noErr)
|
|
{
|
|
msg_Warn(_aout, "CMAudioFormatDescriptionRef creation failure %li", (long)status);
|
|
goto error_avas;
|
|
}
|
|
|
|
_renderer = [[AVSampleBufferAudioRenderer alloc] init];
|
|
if (_renderer == nil)
|
|
goto error;
|
|
|
|
_sync = [[AVSampleBufferRenderSynchronizer alloc] init];
|
|
if (_sync == nil)
|
|
{
|
|
_renderer = nil;
|
|
goto error;
|
|
}
|
|
|
|
[_sync addRenderer:_renderer];
|
|
|
|
_stopped = NO;
|
|
|
|
_ptsSamples = -1;
|
|
_firstPts = VLC_TICK_INVALID;
|
|
_sampleRate = fmt->i_rate;
|
|
_bytesPerFrame = desc.mBytesPerFrame;
|
|
|
|
[notifCenter addObserver:self
|
|
selector:@selector(flushedAutomatically:)
|
|
name:AVSampleBufferAudioRendererWasFlushedAutomaticallyNotification
|
|
object:nil];
|
|
if (@available(macOS 12.0, iOS 15.0, tvOS 15.0, *))
|
|
{
|
|
[notifCenter addObserver:self
|
|
selector:@selector(outputConfigurationChanged:)
|
|
name:AVSampleBufferAudioRendererOutputConfigurationDidChangeNotification
|
|
object:nil];
|
|
}
|
|
|
|
return YES;
|
|
error:
|
|
CFRelease(_fmtDesc);
|
|
error_avas:
|
|
#ifdef HAS_AVAUDIOSESSION
|
|
avas_SetActive(_aout, instance, false,
|
|
AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation);
|
|
#endif
|
|
return NO;
|
|
}
|
|
|
|
@end
|
|
|
|
static int API_AVAILABLE(macos(MIN_MACOS), ios(MIN_IOS), tvos(MIN_TVOS) VISIONOS_API_AVAILABLE)
|
|
DeviceSelect(audio_output_t *aout, const char *name)
|
|
{
|
|
VLCAVSample *sys = (__bridge VLCAVSample*)aout->sys;
|
|
|
|
[sys selectDevice:name];
|
|
|
|
return VLC_SUCCESS;
|
|
}
|
|
|
|
static int API_AVAILABLE(macos(MIN_MACOS), ios(MIN_IOS), tvos(MIN_TVOS) VISIONOS_API_AVAILABLE)
|
|
MuteSet(audio_output_t *aout, bool mute)
|
|
{
|
|
VLCAVSample *sys = (__bridge VLCAVSample*)aout->sys;
|
|
|
|
[sys setMute:mute];
|
|
|
|
return VLC_SUCCESS;
|
|
}
|
|
|
|
static int API_AVAILABLE(macos(MIN_MACOS), ios(MIN_IOS), tvos(MIN_TVOS) VISIONOS_API_AVAILABLE)
|
|
VolumeSet(audio_output_t *aout, float volume)
|
|
{
|
|
VLCAVSample *sys = (__bridge VLCAVSample*)aout->sys;
|
|
|
|
[sys setVolume:volume];
|
|
|
|
return VLC_SUCCESS;
|
|
}
|
|
|
|
static void API_AVAILABLE(macos(MIN_MACOS), ios(MIN_IOS), tvos(MIN_TVOS) VISIONOS_API_AVAILABLE)
|
|
Flush(audio_output_t *aout)
|
|
{
|
|
VLCAVSample *sys = (__bridge VLCAVSample*)aout->sys;
|
|
|
|
[sys flush];
|
|
}
|
|
|
|
static void API_AVAILABLE(macos(MIN_MACOS), ios(MIN_IOS), tvos(MIN_TVOS) VISIONOS_API_AVAILABLE)
|
|
Pause(audio_output_t *aout, bool pause, vlc_tick_t date)
|
|
{
|
|
VLCAVSample *sys = (__bridge VLCAVSample*)aout->sys;
|
|
|
|
[sys pause:pause date:date];
|
|
}
|
|
|
|
static void API_AVAILABLE(macos(MIN_MACOS), ios(MIN_IOS), tvos(MIN_TVOS) VISIONOS_API_AVAILABLE)
|
|
Play(audio_output_t *aout, block_t *block, vlc_tick_t date)
|
|
{
|
|
VLCAVSample *sys = (__bridge VLCAVSample*)aout->sys;
|
|
|
|
[sys play:block date:date];
|
|
}
|
|
|
|
static void API_AVAILABLE(macos(MIN_MACOS), ios(MIN_IOS), tvos(MIN_TVOS) VISIONOS_API_AVAILABLE)
|
|
Stop(audio_output_t *aout)
|
|
{
|
|
VLCAVSample *sys = (__bridge VLCAVSample*)aout->sys;
|
|
|
|
[sys stop];
|
|
}
|
|
|
|
static int API_AVAILABLE(macos(MIN_MACOS), ios(MIN_IOS), tvos(MIN_TVOS) VISIONOS_API_AVAILABLE)
|
|
Start(audio_output_t *aout, audio_sample_format_t *restrict fmt)
|
|
{
|
|
VLCAVSample *sys = (__bridge VLCAVSample*)aout->sys;
|
|
|
|
return [sys start:fmt] ? VLC_SUCCESS : VLC_EGENERIC;
|
|
}
|
|
|
|
static void
|
|
Close(vlc_object_t *obj)
|
|
{
|
|
if (@available(macOS MIN_MACOS, iOS MIN_IOS, tvOS MIN_TVOS VISIONOS_AVAILABLE, *))
|
|
{
|
|
audio_output_t *aout = (audio_output_t *)obj;
|
|
/* Transfer ownership back from VLC to ARC so that it can be released. */
|
|
VLCAVSample *sys = (__bridge_transfer VLCAVSample*)aout->sys;
|
|
(void) sys;
|
|
}
|
|
}
|
|
|
|
static int
|
|
Open(vlc_object_t *obj)
|
|
{
|
|
audio_output_t *aout = (audio_output_t *)obj;
|
|
|
|
if (@available(macOS MIN_MACOS, iOS MIN_IOS, tvOS MIN_TVOS VISIONOS_AVAILABLE, *))
|
|
{
|
|
aout->sys = (__bridge_retained void*) [[VLCAVSample alloc] init:aout];
|
|
if (aout->sys == nil)
|
|
return VLC_EGENERIC;
|
|
|
|
aout->start = Start;
|
|
aout->stop = Stop;
|
|
aout->play = Play;
|
|
aout->pause = Pause;
|
|
aout->flush = Flush;
|
|
aout->volume_set = VolumeSet;
|
|
aout->mute_set = MuteSet;
|
|
aout->device_select = DeviceSelect;
|
|
|
|
return VLC_SUCCESS;
|
|
}
|
|
return VLC_EGENERIC;
|
|
}
|
|
|
|
vlc_module_begin ()
|
|
set_shortname("avsample")
|
|
set_description(N_("AVSampleBufferAudioRenderer output"))
|
|
set_capability("audio output", 100)
|
|
set_subcategory(SUBCAT_AUDIO_AOUT)
|
|
set_callbacks(Open, Close)
|
|
vlc_module_end ()
|
|
|