Yandex.Diskを例として使用して、iOSでオーディオをストリーミングする





オーディオのストリーミングプロジェクトに取り組んでいる間、Yandex.Diskなどの新しいサービスのサポートを追加する必要がありました。 アプリケーションでのオーディオの操作は、URLでファイルを再生し、file、http、httpsなどの標準スキームをサポートするAVPlayerを介して実装されます。 Dropbox、Box、Google Driveなど、認証トークンがリクエストURLに転送されるサービスでは、すべて正常に機能します。 Yandex.Diskなどのサービスの場合、認証トークンはリクエストヘッダーで転送され、AVPlayerはアクセスを提供しません。



既存のAPIの中からこの問題の解決策を見つけると、AVURLAssetでresourceLoaderオブジェクトが使用されることになりました。 これにより、AVPlayerのリモートリソースでホストされているファイルへのアクセスを提供します。 これは、ローカルHTTPプロキシの原則に基づいて機能しますが、使用するのが最も簡単です。



AVPlayerがファイルのダウンロード方法を知らない場合、AVPlayerはresourceLoaderを使用することを理解する必要があります。 したがって、kastumスキームでURLを作成し、このURLでプレーヤーを初期化します。 AVPlayerは、リソースのロード方法を知らず、制御をresourceLoader`yに転送します。



AVAssetResourceLoaderは、2つのメソッドを実装する必要があるAVAssetResourceLoaderDelegateを通じて機能します。



- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest; - (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest;
      
      





最初の呼び出しは、AVAssetResourceLoaderがリソースの読み込みを開始し、AVAssetResourceLoadingRequestを渡すときに呼び出されます。 この場合、リクエストを記憶し、データのダウンロードを開始します。 リクエストが関連しなくなった場合、AVAssetResourceLoaderは2番目のメソッドを呼び出し、データの読み込みをキャンセルします。



最初に、カスタムスキームのURLを使用してAVPlayerを作成し、AVAssetResourceLoaderDelegateとデリゲートメソッドが呼び出されるキューを割り当てます。



 AVURLAsset *asset = [AVURLAsset URLAssetWithURL:[NSURL URLWithString:@"customscheme://host/myfile.mp3"] options:nil]; [asset.resourceLoader setDelegate:self queue:dispatch_get_main_queue()]; AVPlayerItem *item = [AVPlayerItem playerItemWithAsset:asset]; [self addObserversForPlayerItem:item]; self.player = [AVPlayer playerWithPlayerItem:playerItem]; [self addObserversForPlayer];
      
      





リソースのロードは、LSFilePlayerResourceLoaderクラスによって行われます。 ロードされたリソースのURLとYDSessionで初期化され、サーバーからファイルを直接ダウンロードします。 LSFilePlayerResourceLoaderオブジェクトをNSDictionaryに保存し、リソースURLがキーになります。



不明なソースからリソースをロードすると、AVAssetResourceLoaderはデリゲートメソッドを呼び出します。



AVAssetResourceLoaderDelegate
 - (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest{ NSURL *resourceURL = [loadingRequest.request URL]; if([resourceURL.scheme isEqualToString:@"customscheme"]){ LSFilePlayerResourceLoader *loader = [self resourceLoaderForRequest:loadingRequest]; if(loader==nil){ loader = [[LSFilePlayerResourceLoader alloc] initWithResourceURL:resourceURL session:self.session]; loader.delegate = self; [self.resourceLoaders setObject:loader forKey:[self keyForResourceLoaderWithURL:resourceURL]]; } [loader addRequest:loadingRequest]; return YES; } return NO; } - (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest{ LSFilePlayerResourceLoader *loader = [self resourceLoaderForRequest:loadingRequest]; [loader removeRequest:loadingRequest]; }
      
      







起動方法の最初に、回路が私たちのものと一致することを確認します。 次に、LSFilePlayerResourceLoaderをキャッシュから取得するか、新しいファイルを作成して、リソースをロードするリクエストを追加します。



LSFilePlayerResourceLoaderのインターフェイスは次のようになります。



LSFilePlayerResourceLoader
 @interface LSFilePlayerResourceLoader : NSObject @property (nonatomic,readonly,strong)NSURL *resourceURL; @property (nonatomic,readonly)NSArray *requests; @property (nonatomic,readonly,strong)YDSession *session; @property (nonatomic,readonly,assign)BOOL isCancelled; @property (nonatomic,weak)id<LSFilePlayerResourceLoaderDelegate> delegate; - (instancetype)initWithResourceURL:(NSURL *)url session:(YDSession *)session; - (void)addRequest:(AVAssetResourceLoadingRequest *)loadingRequest; - (void)removeRequest:(AVAssetResourceLoadingRequest *)loadingRequest; - (void)cancel; @end @protocol LSFilePlayerResourceLoaderDelegate <NSObject> @optional - (void)filePlayerResourceLoader:(LSFilePlayerResourceLoader *)resourceLoader didFailWithError:(NSError *)error; - (void)filePlayerResourceLoader:(LSFilePlayerResourceLoader *)resourceLoader didLoadResource:(NSURL *)resourceURL; @end
      
      







キューにリクエストを追加/削除するためのメソッドと、すべてのリクエストをキャンセルするためのメソッドが含まれています。 LSFilePlayerResourceLoaderDelegateは、リソースが完全にロードされるか、ロード中にエラーが発生したことを報告します。



addRequestを呼び出してキューにリクエストを追加するとき、pendingRequestsに保存し、データの読み込み操作を開始します。



リクエストを追加する
 - (void)addRequest:(AVAssetResourceLoadingRequest *)loadingRequest{ if(self.isCancelled==NO){ NSURL *interceptedURL = [loadingRequest.request URL]; [self startOperationFromOffset:loadingRequest.dataRequest.requestedOffset length:loadingRequest.dataRequest.requestedLength]; [self.pendingRequests addObject:loadingRequest]; } else{ if(loadingRequest.isFinished==NO){ [loadingRequest finishLoadingWithError:[self loaderCancelledError]]; } } }
      
      







最初に、着信要求ごとに新しいデータ読み込み操作を作成しました。 その結果、データが交差する間にファイルが3つまたは4つのストリームにロードされたことが判明しました。 しかし、AVAssetResourceLoaderが新しいリクエストを開始するとすぐに、以前のリクエストはそれとは無関係になることがわかりました。 これにより、新しい操作を開始するとすぐに、進行中のすべてのデータ読み込み操作を安全にキャンセルでき、トラフィックを節約できます。



サーバーからデータをダウンロードする操作は2つに分かれています。 最初の(contentInfoOperation)は、ファイルのサイズとタイプに関する情報を取得します。 2番目(dataOperation)-オフセット付きのファイルデータを受け取ります。 AVAssetResourceLoadingDataRequestクラスのオブジェクトから、要求されたデータのオフセットとサイズを減算します。



データ読み込み操作
 - (void)startOperationFromOffset:(unsigned long long)requestedOffset length:(unsigned long long)requestedLength{ [self cancelAllPendingRequests]; [self cancelOperations]; __weak typeof (self) weakSelf = self; void(^failureBlock)(NSError *error) = ^(NSError *error) { [weakSelf performBlockOnMainThreadSync:^{ if(weakSelf && weakSelf.isCancelled==NO){ [weakSelf completeWithError:error]; } }]; }; void(^loadDataBlock)(unsigned long long off, unsigned long long len) = ^(unsigned long long offset,unsigned long long length){ [weakSelf performBlockOnMainThreadSync:^{ NSString *bytesString = [NSString stringWithFormat:@"bytes=%lld-%lld",offset,(offset+length-1)]; NSDictionary *params = @{@"Range":bytesString}; id<YDSessionRequest> req = [weakSelf.session partialContentForFileAtPath:weakSelf.path withParams:params response:nil data:^(UInt64 recDataLength, UInt64 totDataLength, NSData *recData) { [weakSelf performBlockOnMainThreadSync:^{ if(weakSelf && weakSelf.isCancelled==NO){ LSDataResonse *dataResponse = [LSDataResonse responseWithRequestedOffset:offset requestedLength:length receivedDataLength:recDataLength data:recData]; [weakSelf didReceiveDataResponse:dataResponse]; } }]; } completion:^(NSError *err) { if(err){ failureBlock(err); } }]; weakSelf.dataOperation = req; }]; }; if(self.contentInformation==nil){ self.contentInfoOperation = [self.session fetchStatusForPath:self.path completion:^(NSError *err, YDItemStat *item) { if(weakSelf && weakSelf.isCancelled==NO){ if(err==nil){ NSString *mimeType = item.path.mimeTypeForPathExtension; CFStringRef contentType = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType,(__bridge CFStringRef)(mimeType),NULL); unsigned long long contentLength = item.size; weakSelf.contentInformation = [[LSContentInformation alloc] init]; weakSelf.contentInformation.byteRangeAccessSupported = YES; weakSelf.contentInformation.contentType = CFBridgingRelease(contentType); weakSelf.contentInformation.contentLength = contentLength; [weakSelf prepareDataCache]; loadDataBlock(requestedOffset,requestedLength); weakSelf.contentInfoOperation = nil; } else{ failureBlock(err); } } }]; } else{ loadDataBlock(requestedOffset,requestedLength); } }
      
      







サーバー上のファイルに関する情報を受け取った後、ネットワークからデータを書き込み、必要に応じてそれらを読み取る一時ファイルを作成します。



ディスクキャッシュの初期化
 - (void)prepareDataCache{ self.cachedFilePath = [[self class] pathForTemporaryFile]; NSError *error = nil; if ([[NSFileManager defaultManager] fileExistsAtPath:self.cachedFilePath] == YES){ [[NSFileManager defaultManager] removeItemAtPath:self.cachedFilePath error:&error]; } if (error == nil && [[NSFileManager defaultManager] fileExistsAtPath:self.cachedFilePath] == NO) { NSString *dirPath = [self.cachedFilePath stringByDeletingLastPathComponent]; [[NSFileManager defaultManager] createDirectoryAtPath:dirPath withIntermediateDirectories:YES attributes:nil error:&error]; if (error == nil) { [[NSFileManager defaultManager] createFileAtPath:self.cachedFilePath contents:nil attributes:nil]; self.writingFileHandle = [NSFileHandle fileHandleForWritingAtPath:self.cachedFilePath]; @try { [self.writingFileHandle truncateFileAtOffset:self.contentInformation.contentLength]; [self.writingFileHandle synchronizeFile]; } @catch (NSException *exception) { NSError *error = [[NSError alloc] initWithDomain:LSFilePlayerResourceLoaderErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey:@"can not write to file"}]; [self completeWithError:error]; return; } self.readingFileHandle = [NSFileHandle fileHandleForReadingAtPath:self.cachedFilePath]; } } if (error != nil) { [self completeWithError:error]; } }
      
      







データパケットを受信した後、まずディスクにキャッシュし、receivedDataLength変数に格納されている受信データのサイズを更新します。 最後に、キュー内のリクエストに新しいデータについて通知します。



データパケットを受信
 - (void)didReceiveDataResponse:(LSDataResonse *)dataResponse{ [self cacheDataResponse:dataResponse]; self.receivedDataLength=dataResponse.currentOffset; [self processPendingRequests]; }
      
      







キャッシュ方法は、目的のオフセットでファイルにデータを書き込みます。



データキャッシング
 - (void)cacheDataResponse:(LSDataResonse *)dataResponse{ unsigned long long offset = dataResponse.dataOffset; @try { [self.writingFileHandle seekToFileOffset:offset]; [self.writingFileHandle writeData:dataResponse.data]; [self.writingFileHandle synchronizeFile]; } @catch (NSException *exception) { NSError *error = [[NSError alloc] initWithDomain:LSFilePlayerResourceLoaderErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey:@"can not write to file"}]; [self completeWithError:error]; } }
      
      







readメソッドは逆の操作を行います。



キャッシュデータの読み取り
 - (NSData *)readCachedData:(unsigned long long)startOffset length:(unsigned long long)numberOfBytesToRespondWith{ @try { [self.readingFileHandle seekToFileOffset:startOffset]; NSData *data = [self.readingFileHandle readDataOfLength:numberOfBytesToRespondWith]; return data; } @catch (NSException *exception) {} return nil; }
      
      









キュー内のリクエストに新しいデータについて通知するために、まずコンテンツに関する情報を記録し、次にキャッシュからデータを記録します。 リクエストのすべてのデータが記録された場合、キューから削除します。



アラートをリクエスト
 - (void)processPendingRequests{ NSMutableArray *requestsCompleted = [[NSMutableArray alloc] init]; for (AVAssetResourceLoadingRequest *loadingRequest in self.pendingRequests){ [self fillInContentInformation:loadingRequest.contentInformationRequest]; BOOL didRespondCompletely = [self respondWithDataForRequest:loadingRequest.dataRequest]; if (didRespondCompletely){ [loadingRequest finishLoading]; [requestsCompleted addObject:loadingRequest]; } } [self.pendingRequests removeObjectsInArray:requestsCompleted]; }
      
      







コンテンツに関する情報を入力する方法では、データの任意の範囲へのアクセスのサイズ、タイプ、フラグを設定します。



コンテンツ情報の入力
 - (void)fillInContentInformation:(AVAssetResourceLoadingContentInformationRequest *)contentInformationRequest{ if (contentInformationRequest == nil || self.contentInformation == nil){ return; } contentInformationRequest.byteRangeAccessSupported = self.contentInformation.byteRangeAccessSupported; contentInformationRequest.contentType = self.contentInformation.contentType; contentInformationRequest.contentLength = self.contentInformation.contentLength; }
      
      







そして、メインのメソッド。キャッシュからデータを読み取り、キューからのリクエストに渡します。



データ入力
 - (BOOL)respondWithDataForRequest:(AVAssetResourceLoadingDataRequest *)dataRequest{ long long startOffset = dataRequest.requestedOffset; if (dataRequest.currentOffset != 0){ startOffset = dataRequest.currentOffset; } // Don't have any data at all for this request if (self.receivedDataLength < startOffset){ return NO; } // This is the total data we have from startOffset to whatever has been downloaded so far NSUInteger unreadBytes = self.receivedDataLength - startOffset; // Respond with whatever is available if we can't satisfy the request fully yet NSUInteger numberOfBytesToRespondWith = MIN(dataRequest.requestedLength, unreadBytes); BOOL didRespondFully = NO; NSData *data = [self readCachedData:startOffset length:numberOfBytesToRespondWith]; if(data){ [dataRequest respondWithData:data]; long long endOffset = startOffset + dataRequest.requestedLength; didRespondFully = self.receivedDataLength >= endOffset; } return didRespondFully; }
      
      







これでブートローダーでの作業が完了しました。 Yandex.Disk SDKをわずかに変更して、サーバー上のファイルから任意の範囲のデータをロードできるようにします。 変更点は3つだけです。



最初に、YDSessionの各リクエストにキャンセルオプションを追加する必要があります。 これを行うには、新しいYDSessionRequestプロトコルを追加し、リクエストの戻り値として設定します。



YDSession.h


 @protocol YDSessionRequest <NSObject> - (void)cancel; @end - (id<YDSessionRequest>)fetchDirectoryContentsAtPath:(NSString *)path completion:(YDFetchDirectoryHandler)block; - (id<YDSessionRequest>)fetchStatusForPath:(NSString *)path completion:(YDFetchStatusHandler)block;
      
      







2番目-サーバー上のファイルから任意の範囲データをロードするメソッドを追加します。



YDSession.h


 - (id<YDSessionRequest>)partialContentForFileAtPath:(NSString *)srcRemotePath withParams:(NSDictionary *)params response:(YDDidReceiveResponseHandler)response data:(YDPartialDataHandler)data completion:(YDHandler)completion;
      
      







YDSession.m


 - (id<YDSessionRequest>)partialContentForFileAtPath:(NSString *)srcRemotePath withParams:(NSDictionary *)params response:(YDDidReceiveResponseHandler)response data:(YDPartialDataHandler)data completion:(YDHandler)completion{ return [self downloadFileFromPath:srcRemotePath toFile:nil withParams:params response:response data:data progress:nil completion:completion]; } - (id<YDSessionRequest>)downloadFileFromPath:(NSString *)path toFile:(NSString *)aFilePath withParams:(NSDictionary *)params response:(YDDidReceiveResponseHandler)responseBlock data:(YDPartialDataHandler)dataBlock progress:(YDProgressHandler)progressBlock completion:(YDHandler)completionBlock{ NSURL *url = [YDSession urlForDiskPath:path]; if (!url) { completionBlock([NSError errorWithDomain:kYDSessionBadArgumentErrorDomain code:0 userInfo:@{@"getPath": path}]); return nil; } BOOL skipReceivedData = NO; if(aFilePath==nil){ aFilePath = [[self class] pathForTemporaryFile]; skipReceivedData = YES; } NSURL *filePath = [YDSession urlForLocalPath:aFilePath]; if (!filePath) { completionBlock([NSError errorWithDomain:kYDSessionBadArgumentErrorDomain code:1 userInfo:@{@"toFile": aFilePath}]); return nil; } YDDiskRequest *request = [[YDDiskRequest alloc] initWithURL:url]; request.fileURL = filePath; request.params = params; request.skipReceivedData = skipReceivedData; [self prepareRequest:request]; NSURL *requestURL = [request.URL copy]; request.callbackQueue = _callBackQueue; request.didReceiveResponseBlock = ^(NSURLResponse *response, BOOL *accept) { if(responseBlock){ responseBlock(response); } }; request.didGetPartialDataBlock = ^(UInt64 receivedDataLength, UInt64 expectedDataLength, NSData *data){ if(progressBlock){ progressBlock(receivedDataLength,expectedDataLength); } if(dataBlock){ dataBlock(receivedDataLength,expectedDataLength,data); } }; request.didFinishLoadingBlock = ^(NSData *receivedData) { if(skipReceivedData){ [[self class] removeTemporaryFileAtPath:aFilePath]; } NSDictionary *userInfo = @{@"URL": requestURL, @"receivedDataLength": @(receivedData.length)}; [[NSNotificationCenter defaultCenter] postNotificationInMainQueueWithName:kYDSessionDidDownloadFileNotification object:self userInfo:userInfo]; completionBlock(nil); }; request.didFailBlock = ^(NSError *error) { if(skipReceivedData){ [[self class] removeTemporaryFileAtPath:aFilePath]; } NSDictionary *userInfo = @{@"URL": requestURL}; [[NSNotificationCenter defaultCenter] postNotificationInMainQueueWithName:kYDSessionDidFailToDownloadFileNotification object:self userInfo:userInfo]; completionBlock([NSError errorWithDomain:error.domain code:error.code userInfo:userInfo]); }; [request start]; NSDictionary *userInfo = @{@"URL": request.URL}; [[NSNotificationCenter defaultCenter] postNotificationInMainQueueWithName:kYDSessionDidStartDownloadFileNotification object:self userInfo:userInfo]; return (id<YDSessionRequest>)request; }
      
      







3番目に修正する必要があるのは、コールバックキューをパラレルからシリアルに変更することです。そうしないと、データブロックが要求した順序で到着せず、ユーザーが音楽を再生するときにぎこちなく聞こえます。



YDSession.m


 - (instancetype)initWithDelegate:(id<YDSessionDelegate>)delegate callBackQueue:(dispatch_queue_t)queue{ self = [super init]; if (self) { _delegate = delegate; _callBackQueue = queue; } return self; } YDDiskRequest *request = [[YDDiskRequest alloc] initWithURL:url]; request.fileURL = filePath; request.params = params; [self prepareRequest:request]; request.callbackQueue = _callBackQueue;
      
      









GitHubのサンプルソースコード。



All Articles