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