forked from mpv-player/mpv
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
ao_avplayer: output audio on macOS via AVPlayer
- Loading branch information
Showing
3 changed files
with
370 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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), | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters