[Oculus]3D動画をストリーミング再生する方法 – 7 –

数週間ほど更新が出来ておらず、申し訳ございません。
今週から再開いたしますので、これまでご覧いただいていた方は
今後ともよろしくお願いいたします。

前回は、フレーム画像をサーバーにアップロードする方法をご紹介しました。
今回は、フレーム保存からアップロード処理の改善を行いました。

前回までの記事で、フレーム毎に画像を保存し、保存した画像をサーバーへアップロードする処理の一通りは出来ていましたが、
1秒当たりのアップロードファイル数が極端に少なく実用性がありませんでした。
主に以下の障害が発生していました。

  • 1秒あたりのアップロード数が1枚
  • カメラで写しているものの表示が異常にカクつく

上記が発生していた原因としては、フレーム保存を同期処理で行っていたことと、フレーム保存のたびに通信を行っていたことにあります。
画像保存前のデータ及び送信データがNSDataであるため、単純にフレーム処理自体をやめて生成したNSDataをそのままアップロードデータとして設定することで
フレーム保存処理をまるごと省略することができます。
この方法をとった場合、フレーム毎に通信を行う件を解決するためには、生成したNSDataを配列で一時的に保持し、監視タイマーを用意して一定枚数毎に通信を行うように制御する必要が有ります。

しかし実際に動作させてみたところ、アップロード速度がフレーム毎のUIImageの保持速度に間に合わず
数十秒撮影し続けると、保持しているデータが膨大になり、メモリ使用量がみるみるうちに増え、エラーが発生してしまいます。

エラーは発生してしまったものの、アップロード処理を制御することで、パフォーマンスの改善がみられたため
ファイルの保存処理を別の方法で解決することで、残りの問題も解決できそうです。

保存時負荷を防ぐために、NSDataを書き込まずに配列で保持して、使用するよう変更しましたが、
メモリ使用量の問題が発生しては意味がないため、アップロードファイルの保持は元の通りローカルストレージへの一時保存に戻します。
この件で、パフォーマンスに影響が出ているのは「同期」で保存処理を実行しているためなので「カメラ入力の画面表示」「一時的な保存処理」を行うためのスレッドをそれぞれ用意し
並列で処理を実行することで解決することができます。

通信のキュー化やサブスレッド・非同期処理について、いろいろなサイトで説明がされているため、詳しくはそちらをご覧ください。

今回の対応で変更となった箇所について、その部分だけを抜粋して記載します。

■通信キュー化

//監視タイマー
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:0.1
                                            target:self
                                            selector:@selector(update)
                                            userInfo:nil
                                            repeats:YES];

- (void)update
{
    if (self.isCapturing && self.connectCnt < 1)
    {
        //一時保存した画像を取得
        NSString *path = [self getImagePath:(long)self.cnt];
        UIImage *image = [UIImage imageWithContentsOfFile:path];
        if (image == nil) {
            NSLog(@"imageが見つかりません");
            return;
        }

        //送信用にjpegからNSDataを生成
        NSData *data = UIImageJPEGRepresentation(image, 0.2);
        NSLog(@"##[ start upload ]## num => %04d", self.cnt);

        //一時保存した画像を削除
        NSFileManager *fileManager = [[NSFileManager alloc] init];
        [fileManager removeItemAtPath:path error:NULL];

        // 送信データの境界
        NSString *boundary = @"1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
        NSString *urlString = @"http://192.168.11.3/index.php";
        // アップロードする際のパラメーター名とファイル名
        NSString *uploadName = @"postfile";
        NSString *uploadFileName = [NSString stringWithFormat:@"%08d", self.cnt];
        NSMutableString *sendDataStringPrev = [NSMutableString stringWithString:@"--"];
        [sendDataStringPrev appendString:boundary];
        [sendDataStringPrev appendString:@"\r\n"];
        [sendDataStringPrev appendString:[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"; filename=\"%@.jpg\"\r\n",uploadName,uploadFileName]];
        [sendDataStringPrev appendString:@"Content-Type: image/jpeg\r\n\r\n"];
        NSMutableString *sendDataStringNext = [NSMutableString stringWithString:@"\r\n"];
        [sendDataStringNext appendString:@"--"];
        [sendDataStringNext appendString:boundary];
        [sendDataStringNext appendString:@"--"];

        // 送信データの生成
        NSMutableData *sendData = [NSMutableData data];
        [sendData appendData:[sendDataStringPrev dataUsingEncoding:NSUTF8StringEncoding]];
        [sendData appendData:data];
        [sendData appendData:[sendDataStringNext dataUsingEncoding:NSUTF8StringEncoding]];

        // リクエストヘッダー
        NSDictionary *requestHeader = [NSDictionary dictionaryWithObjectsAndKeys:
                                       [NSString stringWithFormat:@"%lu",(unsigned long)[sendData length]],@"Content-Length",
                                       [NSString stringWithFormat:@"multipart/form-data;boundary=%@",boundary],@"Content-Type",nil];

        NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:urlString]];
        [request setAllHTTPHeaderFields:requestHeader];
        [request setHTTPMethod:@"POST"];
        [request setHTTPBody:sendData];

        self.cnt++;
        self.connectCnt++;

        NSOperationQueue *queue = [[NSOperationQueue alloc] init];
        [NSURLConnection sendAsynchronousRequest:request queue:queue completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
            self.connectCnt--;
        }];
    }
}

■サブスレッド化

_main_queue = dispatch_get_main_queue();
_write_image_queue = dispatch_queue_create("write_file_queue", 0);
_sub_queue = dispatch_queue_create("sub_queue", 0);

//上記通信部を以下のブロックで囲う
    // 画像を画面に表示
    dispatch_async(
        _sub_queue, ^{

        });

//画像保存処理部
NSString *path = [self getImagePath:(long)index];

//jpeg作成
NSData *data = UIImageJPEGRepresentation(image, 1.0);
dispatch_async(_write_image_queue, ^{
    BOOL result = [data writeToFile:path atomically:YES];
});

終わった後に改めて考えると、なんとも簡単な方法でした。
実際に作業しているときには、なかなか気付けないものですね。

これで、スマホ側のアップロード処理が使えるレベルまで改善されたため、残りはサーバー側の受け取った画像での動画作成周りを終わらせることができれば、終わりです。
しばらく出てきていないOculusが登場する日も遠くはなくなってきたため、皆さん楽しみにしていてください。

以上で、パフォーマンス改善は終了となります。
最後までご覧いただきありがとうございました。


弊社では全国各地の請負い(ご自宅)で作業協力頂ける、フリーランスエンジニアの方を常時探しております。
ご興味ある方は、お気軽にお問い合わせ下さい。


コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

*