Skip to content

Commit

Permalink
ao_avplayer: output audio on macOS via AVPlayer
Browse files Browse the repository at this point in the history
  • Loading branch information
tmm1 committed Dec 16, 2021
1 parent 3ec2012 commit 96460cf
Show file tree
Hide file tree
Showing 3 changed files with 370 additions and 0 deletions.
2 changes: 2 additions & 0 deletions audio/out/ao.c
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
extern const struct ao_driver audio_out_oss;
extern const struct ao_driver audio_out_audiotrack;
extern const struct ao_driver audio_out_audiounit;
extern const struct ao_driver audio_out_avplayer;
extern const struct ao_driver audio_out_coreaudio;
extern const struct ao_driver audio_out_coreaudio_exclusive;
extern const struct ao_driver audio_out_rsound;
Expand All @@ -62,6 +63,7 @@ static const struct ao_driver * const audio_out_drivers[] = {
#endif
#if HAVE_COREAUDIO
&audio_out_coreaudio,
&audio_out_avplayer,
#endif
#if HAVE_PULSE
&audio_out_pulse,
Expand Down
367 changes: 367 additions & 0 deletions audio/out/ao_avplayer.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,367 @@
/*
* Copyright (C) 2021 Aman Karmani
* Copyright (C) 2019 Scott Davilla
*
* This file is part of mpv.
*
* mpv 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.
*
* mpv 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 mpv. If not, see <http://www.gnu.org/licenses/>.
*/

#include "config.h"
#include "ao.h"
#include "internal.h"
#include "audio/format.h"
#include "osdep/timer.h"
#include "options/m_option.h"
#include "common/msg.h"
#include "ao_coreaudio_utils.h"

#import <AudioToolbox/AudioQueue.h>
#import <CoreAudio/CoreAudioTypes.h>
#import <AudioToolbox/AudioToolbox.h>
#import <AVFoundation/AVFoundation.h>
#import <libavutil/bswap.h>
#import <libavutil/intfloat.h>
#import <mach/mach_time.h>
#import <sys/stat.h>

@class AVPlayerSink;
@class AudioResourceLoader;

struct priv {
AVPlayerSink *player;
pthread_mutex_t lock;
pthread_cond_t wait; // paired with lock
};

@interface AVPlayerSink : NSObject
@property (nonatomic, assign) struct ao *ao;
@property (nonatomic, assign) BOOL loaded;
@property (nonatomic, assign) dispatch_queue_t queue;
@property (nonatomic, strong) AVPlayer *avplayer;
@property (nonatomic, strong) AVPlayerItem *playerItem;
@property (nonatomic, strong) AudioResourceLoader *avLoader;
- (id)initWithAO:(struct ao *)ao;
- (void)close;
- (void)play;
- (void)pause;
- (void)start;
- (void)flush;
- (double)currentTime;
@end

@interface AudioResourceLoader : NSObject <AVAssetResourceLoaderDelegate>
@property (nonatomic, assign) AVPlayerSink *sink;
@property (atomic, assign) BOOL aborted;
@property (atomic, assign) int64_t cursor;
@property (atomic, assign) int64_t enqueued;
- (id)initWithSink:(AVPlayerSink *)sink;
- (void)abort;
@end

@implementation NSMutableData (helpers)
- (void)appendB64:(uint64_t)data{
data = av_bswap64(data);
uint8_t *bytes = (uint8_t *)&data;
[self appendBytes:bytes length:8];
}
- (void)appendB32:(uint32_t)data{
data = av_bswap32(data);
uint8_t *bytes = (uint8_t *)&data;
[self appendBytes:bytes length:4];
}
@end

static void await(struct ao *ao, double timeout)
{
struct priv *p = ao->priv;
struct timespec ts = mp_rel_time_to_timespec(timeout);
pthread_mutex_lock(&p->lock);
pthread_cond_timedwait(&p->wait, &p->lock, &ts);
pthread_mutex_unlock(&p->lock);
}

@implementation AudioResourceLoader
- (id)initWithSink:(AVPlayerSink *)sink{
self = [super init];
if (self) {
self.sink = sink;
}
return self;
}

- (void)abort {
self.aborted = YES;
}

- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest {
struct ao *ao = self.sink.ao;

if (self.aborted) {
return NO;
}

AVAssetResourceLoadingContentInformationRequest *contentRequest = loadingRequest.contentInformationRequest;
if (contentRequest) {
contentRequest.contentType = @"com.apple.coreaudio-format";
contentRequest.contentLength = UINT_MAX;
contentRequest.byteRangeAccessSupported = NO;
}

AVAssetResourceLoadingDataRequest *dataRequest = loadingRequest.dataRequest;
if (dataRequest) {
size_t offset = dataRequest.requestedOffset;
if ((offset == 0 && dataRequest.requestedLength == 2) || // probing initial header
offset != self.cursor) { // or, probing near end of file far from our cursor
if (offset > 0)
[dataRequest respondWithData:[NSData dataWithBytes:"\x00\x00" length:2]];
else
[dataRequest respondWithData:[NSData dataWithBytes:"ca" length:2]];
[loadingRequest finishLoading];
return YES;
}

if (offset == 0) {
// CAF header (see https://developer.apple.com/library/archive/documentation/MusicAudio/Reference/CAFSpec/CAF_spec/CAF_spec.html)
NSMutableData *data = [NSMutableData dataWithBytes:"caff\x00\x01\x00\x00" length:8];
[data appendBytes:"desc" length:4];
[data appendB64:32];
AudioStreamBasicDescription asbd = {0};
ca_fill_asbd(ao, &asbd);
[data appendB64:av_double2int(asbd.mSampleRate)];
[data appendB32:asbd.mFormatID];
[data appendB32:3 /*kCAFLinearPCMFormatFlagIsFloat | kCAFLinearPCMFormatFlagIsLittleEndian*/];
[data appendB32:asbd.mBytesPerPacket];
[data appendB32:asbd.mFramesPerPacket];
[data appendB32:asbd.mChannelsPerFrame];
[data appendB32:asbd.mBitsPerChannel];
[data appendBytes:"data" length:4];
[data appendB64:-1];
[data appendB32:0];
[dataRequest respondWithData:data];
[loadingRequest finishLoading];
self.cursor += data.length;
return YES;
}

dispatch_async(self.sink.queue, ^{
[self process:loadingRequest];
});
}

return YES;
}

- (void)process:(AVAssetResourceLoadingRequest *)loadingRequest{
AVAssetResourceLoadingDataRequest *dataRequest = loadingRequest.dataRequest;
struct ao *ao = self.sink.ao;
double bufferedTime = self.enqueued / (double)ao->samplerate;

while (!self.aborted) {
bool ready = false;
double delay = bufferedTime - self.sink.currentTime;
if (delay < 2.0f) {
ready = true;
}
if (ready || self.aborted)
break;
await(ao, 0.250);
}

char buffer[65536];
void *buf = &buffer;

while (!self.aborted) {
int requested = sizeof(buffer) / ao->sstride;
int64_t delay = (bufferedTime - self.sink.currentTime) * 1e6;
delay += ca_frames_to_us(ao, requested);
int samples = ao_read_data(ao, &buf, requested, mp_time_us() + delay);
if (samples > 0) {
NSData *data = [NSData dataWithBytes:buf length:samples * ao->sstride];
[dataRequest respondWithData:data];
self.cursor += data.length;
self.enqueued += samples;
[loadingRequest finishLoading];
return;
}
await(ao, 0.250);
}
}

- (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest {
self.aborted = YES;
}
@end

@implementation AVPlayerSink
- (id)initWithAO:(struct ao *)ao {
self = [super init];
if (self) {
self.ao = ao;
self.queue = dispatch_queue_create("mpv/avplayer sink", DISPATCH_QUEUE_SERIAL);
self.avplayer = AVPlayer.new;
}
return self;
}

- (void)close {
[self.playerItem removeObserver:self forKeyPath:@"status"];
[self.avplayer pause];
[self.avplayer.currentItem.asset cancelLoading];
[self.avplayer replaceCurrentItemWithPlayerItem:nil];
[self.playerItem.asset cancelLoading];
[self.avLoader abort];

dispatch_sync(self.queue, ^{
self.avplayer = nil;
self.playerItem = nil;
self.avLoader = nil;
});
}

- (void)dealloc {
[self close];
dispatch_release(self.queue);
[super dealloc];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
struct priv *p = self.ao->priv;
if (object == self.playerItem && [keyPath isEqualToString:@"status"]) {
if (self.avplayer.currentItem.status == AVPlayerItemStatusReadyToPlay) {
self.loaded = true;
MP_VERBOSE(self.ao, "ready to start playback @ %fs buffered\n", self.bufferedTime);
pthread_cond_broadcast(&p->wait);
}
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}

- (void)play {
[self.avplayer playImmediatelyAtRate:1.0];
}

- (void)pause {
[self.avplayer pause];
}

- (void)start {
struct priv *p = self.ao->priv;
dispatch_sync(self.queue, ^{
NSString *url = [NSString stringWithFormat:@"mpv://avplayer/streams/%ld.caf",
(long)NSDate.now.timeIntervalSince1970];
NSURL *streamURL = [NSURL URLWithString:url];
AVURLAsset *asset = [AVURLAsset URLAssetWithURL:streamURL options:nil];
self.avLoader = [[AudioResourceLoader alloc] initWithSink:self];
[asset.resourceLoader setDelegate:self.avLoader queue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)];

self.playerItem = [AVPlayerItem playerItemWithAsset:asset];
[self.playerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];

self.loaded = false;
[self.avplayer replaceCurrentItemWithPlayerItem:self.playerItem];
self.avplayer.actionAtItemEnd = AVPlayerActionAtItemEndNone;
self.avplayer.automaticallyWaitsToMinimizeStalling = NO;
self.avplayer.currentItem.canUseNetworkResourcesForLiveStreamingWhilePaused = YES;
});
pthread_cond_broadcast(&p->wait);
}

- (void)flush {
struct priv *p = self.ao->priv;
[self.playerItem removeObserver:self forKeyPath:@"status"];
[self.avplayer pause];
[self.avplayer replaceCurrentItemWithPlayerItem:nil];
[self.playerItem.asset cancelLoading];
[self.avLoader abort];

pthread_cond_broadcast(&p->wait);

dispatch_sync(self.queue, ^{
self.playerItem = nil;
self.avLoader = nil;
});

[self start];
}

- (double)currentTime {
return CMTimeGetSeconds(self.playerItem.currentTime);
}

- (double)bufferedTime {
double cursorTime = self.avLoader.enqueued / (double)self.ao->samplerate;
return cursorTime - self.currentTime;
}
@end

static bool init_avplayer(struct ao *ao)
{
struct priv *p = ao->priv;

pthread_mutex_init(&p->lock, NULL);
pthread_cond_init(&p->wait, NULL);

ao->format = AF_FORMAT_FLOAT;
ao->channels = (struct mp_chmap)MP_CHMAP_INIT_STEREO;
p->player = [AVPlayerSink.alloc initWithAO:ao];
[p->player start];

return true;
}

static void uninit(struct ao *ao)
{
struct priv *p = ao->priv;

[p->player close];
[p->player release];
p->player = nil;

pthread_cond_destroy(&p->wait);
pthread_mutex_destroy(&p->lock);
}

static int init(struct ao *ao)
{
if (!init_avplayer(ao))
return CONTROL_ERROR;
return CONTROL_OK;
}

static void reset(struct ao *ao)
{
struct priv *p = ao->priv;
[p->player flush];
}

static void start(struct ao *ao)
{
struct priv *p = ao->priv;
[p->player play];
pthread_cond_broadcast(&p->wait);
}

#define OPT_BASE_STRUCT struct priv

const struct ao_driver audio_out_avplayer = {
.description = "AVPlayer (macOS)",
.name = "avplayer",
.uninit = uninit,
.init = init,
.reset = reset,
.start = start,
.priv_size = sizeof(struct priv),
};
1 change: 1 addition & 0 deletions wscript_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ def swift(task):
( "audio/out/ao_alsa.c", "alsa" ),
( "audio/out/ao_audiotrack.c", "android" ),
( "audio/out/ao_audiounit.m", "audiounit" ),
( "audio/out/ao_avplayer.m", "coreaudio" ),
( "audio/out/ao_coreaudio.c", "coreaudio" ),
( "audio/out/ao_coreaudio_chmap.c", "coreaudio || audiounit" ),
( "audio/out/ao_coreaudio_exclusive.c", "coreaudio" ),
Expand Down

0 comments on commit 96460cf

Please sign in to comment.