AVPlayer загружает AVAsset из файла, который добавляется одновременно внешним источником (для macOS и iOS)

У меня есть вопрос об использовании AVFoundations AVPlayer (вероятно, применим как к iOS, так и к macOS). Я пытаюсь воспроизвести аудио (несжатые wav) данные, которые поступают из канала, отличного от стандартного потокового HTTP-потока.

Случай:
Пакеты аудиоданных сжимаются в канале вместе с другими данными, с которыми приложение должно работать. Например, видео и аудио поступают в один канал и разделяются заголовком.
После фильтрации я получаю аудиоданные и распаковываю их в WAV-формат (на данном этапе не содержит заголовков).
После того, как пакеты данных готовы (9600 байт для 24k, стерео 16-битный звук), они передаются в экземпляр AVPlayer (AVAudioPlayer в соответствии с Apple не подходит для потоковой передачи звука).

Учитывая, что AVPlayer (элемент или объект) не загружается из памяти (нет initWithData: (NSData)) и требует URL-адреса HTTP Live Stream или URL-адреса файла, я создаю файл на диске (либо macOS, либо iOS) добавьте WAV Headers и добавьте несжатые данные там.

Вернемся к AVPlayer, создав следующее:

AVURLAsset *audioAsset = [[AVURLAsset alloc] initWithURL:[NSURL fileURLWithPath:tempAudioFile] options:nil];
AVPlayerItem *audioItem = [[AVPlayerItem alloc] initWithAsset:audioAsset];
AVPlayer *audioPlayer = [[AVPlayer alloc] initWithPlayerItem:audioItem];

добавление KVO, а затем попытка начать воспроизведение:

[audioPlayer play];

В результате звук воспроизводится в течение 1-2 секунд, а затем останавливается (AVPlayerItemDidPlayToEndTimeNotification), а данные продолжают добавляться в файл. Поскольку все это в цикле, [воспроизведение аудиопользователя] запускается и приостанавливается (скорость == 0) несколько раз.

Вся концепция в упрощенной форме:

-(void)PlayAudioWithData:(NSData *data) //data in encoded format
{
    NSData *decodedSound = [AudioDecoder DecodeData:data]; //decodes the data from the compressed format (Opus) to WAV
    [Player CreateTemporaryFiles]; //This creates the temporary file by appending the header and waiting for input.

    [Player SendDataToPlayer:decodedSound]; //this sends the decoded data to the Player to be stored to file. See below for appending.

    Boolean prepared = [Player isPrepared]; //a check if AVPlayer, Item and Asset are initialized
    if (!prepared)= [Player Prepare]; //creates the objects like above
    Boolean playing = [Player isAudioPlaying]; //a check done on the AVPlayer if rate == 1
    if (!playing) [Player startPlay]; //this is actually [audioPlayer play]; on AVPlayer Instance
}

-(void)SendDataToPlayer:(NSData *data)
{
    //Two different methods here. First with NSFileHandle — not so sure about this though as it definitely locks the file.
    //Initializations and deallocations happen elsewhere, just condensing code to give you an idea
    NSFileHandle *audioFile = [NSFileHandle fileHandleForWritingAtPath:_tempAudioFile]; //happens else where
    [audioFile seekToEndOfFile];
    [audioFile writeData:data];
    [audioFile closeFile]; //happens else where

    //Second method is 
    NSOutputStream *audioFileStream = [NSOutputStream outputStreamWithURL:[NSURL fileURLWithPath:_tempStreamFile] append:YES];
    [audioFileStream open];
    [audioFileStream write:[data bytes] maxLength:data.length];
    [audioFileStream close];
}

Оба NSFileHandle и NSOutputStream полностью работают с WAV файлами, исполняемыми QuickTime, iTunes, VLC и т.д. Кроме того, если я обойду [Player SendDataToPlayer: decodedSound] и имею временный аудиофайл, предварительно загруженный стандартным WAV, он также воспроизводится нормально.

До сих пор существуют две стороны: a) У меня звуковые данные распакованы и готовы к воспроизведению b) Я правильно сохраняю данные.

То, что я пытаюсь сделать, это send-write-read в строке. Это заставляет меня думать, что сохранение данных в файл, получает эксклюзивный доступ к файловому ресурсу и не позволяет AVPlayer продолжать воспроизведение.

Кто-нибудь имеет представление о том, как сохранить файл доступным как для NSFileHandle/NSOutputStream, так и для AVPlayer?

Или даже лучше... Есть ли AVPlayer initWithData? (Хе-хе...)

Любая помощь очень ценится! Спасибо заранее.

Ответ 1

Вы можете использовать AVAssetResourceLoader для передачи своих собственных данных и метаданных в AVAsset, которые затем можно воспроизводить с помощью AVPlayer, в результате создавая [[AVPlayer alloc] initWithData:...]:

- (AVPlayer *)playerWithWavData:(NSData* )wavData {
    self.strongDelegateReference = [[NSDataAssetResourceLoaderDelegate alloc] initWithData:wavData contentType:AVFileTypeWAVE];

    NSURL *url = [NSURL URLWithString:@"ns-data-scheme://"];
    AVURLAsset *asset = [[AVURLAsset alloc] initWithURL:url options:nil];

    // or some other queue != main queue
    [asset.resourceLoader setDelegate:self.strongDelegateReference queue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)];

    AVPlayerItem *item = [[AVPlayerItem alloc] initWithAsset:asset];
    return [[AVPlayer alloc] initWithPlayerItem:item];
}

который вы можете использовать так:

[self setupAudioSession];

NSURL *wavUrl = [[NSBundle mainBundle] URLForResource:@"foo" withExtension:@"wav"];
NSData *wavData = [NSData dataWithContentsOfURL:wavUrl];

self.player = [self playerWithWavData:wavData];

[self.player play];

Дело в том, что AVAssetResourceLoader очень мощный (если вы не хотите использовать AirPlay), поэтому вы, вероятно, можете лучше, чем подавать аудиоданные на AVPlayer в одном компе - вы можете передать его в делегат AVAssetResourceLoader по мере его доступности.

Вот простой делегат AVAssetResourceLoader. Чтобы изменить его для потоковой передачи, должно быть достаточно установить более длинный contentLength, чем объем данных, которые у вас есть.

Заголовочный файл:

#import <Foundation/Foundation.h>
#import <AVFoundation/AVFoundation.h>

@interface NSDataAssetResourceLoaderDelegate : NSObject <AVAssetResourceLoaderDelegate>

- (instancetype)initWithData:(NSData *)data contentType:(NSString *)contentType;

@end

Файл реализации:

@interface NSDataAssetResourceLoaderDelegate()

@property (nonatomic) NSData *data;
@property (nonatomic) NSString *contentType;

@end

@implementation NSDataAssetResourceLoaderDelegate

- (instancetype)initWithData:(NSData *)data contentType:(NSString *)contentType {
    if (self = [super init]) {
        self.data = data;
        self.contentType = contentType;
    }
    return self;
}

- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest {
    AVAssetResourceLoadingContentInformationRequest* contentRequest = loadingRequest.contentInformationRequest;

    // TODO: check that loadingRequest.request is actually our custom scheme        

    if (contentRequest) {
        contentRequest.contentType = self.contentType;
        contentRequest.contentLength = self.data.length;
        contentRequest.byteRangeAccessSupported = YES;
    }

    AVAssetResourceLoadingDataRequest* dataRequest = loadingRequest.dataRequest;

    if (dataRequest) {
        // TODO: handle requestsAllDataToEndOfResource
        NSRange range = NSMakeRange((NSUInteger)dataRequest.requestedOffset, (NSUInteger)dataRequest.requestedLength);
        [dataRequest respondWithData:[self.data subdataWithRange:range]];
        [loadingRequest finishLoading];
    }

    return YES;
}

@end