Open10

【Unity】自分の音声を録音し、AWS S3にwav形式でアップロードする

だーら(Flamers / Memotia)だーら(Flamers / Memotia)

AWS SDK for .NETのインストール

  • こちらの公式ページより、aws-sdk-netstandard2.0.zipをダウンロード → 解凍
  • Assetsの下にAWSというフォルダを作成し、以下の2つのdllを入れる
    • AWSSDK.Core.dll
    • AWSSDK.S3.dll
  • さらに、上記のdllの依存関係を解決するために、以下の3つのdllも同じく入れる
    • Microsoft.Bcl.AsyncInterfaces.dll
    • System.Runtime.CompilerServices.Unsafe.dll
    • System.Threading.Tasks.Extensions.dll
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

AWSでの設定

  • 新規バケットを作成
    • バケット名以外はデフォの設定を変更していない(セキュアなリポジトリ)
  • S3へのアクセス権限を持ったIAMを作成
    • とりあえずAmazonS3FullAccessポリシーのみをアタッチしておく
  • アクセスキーを発行し、シークレットキーとともに控えておく
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

コードの実装

  • AWSの公式doc
  • 作成するクラスは主に2つで(名前は...もっと考えます)
    • VoiceAnalyzeManager: レコーディングの開始・終了や、S3アップロードの指示など起点となるクラス
    • AWSS3Uploader: S3へのファイルアップロードを担当するクラス
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

実装: VoiceAnalyzeManager

  • wav形式への変換については、こちらの記事を参考にしました。
  • 録音が開始されたあとにマイクデバイスを変更した場合、録音が停止してしまうという問題が未解決です。現状録音に使われているデバイスから、同じデバイスへ変更する指示がいった場合も、録音が停止されることが分かりました。
using UnityEngine;
using System.IO;

public class VoiceAnalyzeManager : MonoBehaviour
{
    private AudioClip _recordedClip;
    private string _micDeviceName = "";
    private const int SamplingFrequency = 11025; //サンプリング周波数: 44100がデフォっぽいが容量削減で下げている
    private string _temporaryFilePath = "";
    private AWSS3Uploader _awsS3Uploader;

    void Start()
    {
        _awsS3Uploader = new AWSS3Uploader();
    }

    // 現在利用しているマイクが変更されたときの処理
    // ただし、録音中に変更されると録音が停止されるという問題がある
    public void SetMicDeviceName(string deviceName)
    {
        _micDeviceName = deviceName;
    }
    
    public void StartRecording(int lengthSec)
    {
        _recordedClip = Microphone.Start(deviceName: _micDeviceName, loop: false, lengthSec: lengthSec, frequency: SamplingFrequency);
    }

    public void FinishRecording()
    {
        if (Microphone.IsRecording(deviceName: _micDeviceName))
        {
            Microphone.End(deviceName: _micDeviceName);
        }
        else
        {
            return;
        }
    
        // wav形式に変換する
        byte[] recordWavData = WavConverter.ToWav(_recordedClip);

        _temporaryFilePath = Path.Combine(Application.temporaryCachePath, "MyRecording.wav");
        File.WriteAllBytes(_temporaryFilePath, recordWavData);
        
        UploadAudioToS3();
    }

    private void UploadAudioToS3()
    {
        // この辺のファイル名等はいい感じに調整
        string objectName = "MyRecordingUploaded.wav";
        _awsS3Uploader.StartUpload(objectName, _temporaryFilePath);
    }

    // tmpに保存してあったaudioデータを削除する
    void OnDestroy()
    {
        if (File.Exists(_temporaryFilePath))
        {
            File.Delete(_temporaryFilePath);
        }
    }
}   
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

実装: AWSS3Uploader

  • TODO: キャンセル処理をちゃんとすべき
  • access_keyやsecret_keyはビルドファイルに組み込むべきではないので、別途対応が必要。
using Amazon;
using Amazon.S3;
using Amazon.S3.Model;
using Cysharp.Threading.Tasks;

public class AWSS3Uploader
{
    private readonly IAmazonS3 _s3Client;
    private string _objectName = "";
    private string _filePath = "";

    // const
    private const string BucketName = "bucket-name";
    private static readonly RegionEndpoint BucketRegion = RegionEndpoint.APNortheast1;
    
    // credentials
    private const string S3AccessKey = "";
    private const string S3SecretKey = "";

    public AWSS3Uploader()
    {
        _s3Client = new AmazonS3Client(S3AccessKey, S3SecretKey, BucketRegion);
    }
    
    public async void StartUpload(string objectName, string filePath)
    {
        _objectName = objectName;
        _filePath = filePath;
        await UploadFileAsync();
    }

    private async UniTask UploadFileAsync()
    {
        PutObjectRequest request = new PutObjectRequest()
        {
            BucketName = BucketName,
            Key = _objectName,
            FilePath = _filePath
        };
    
        await _s3Client.PutObjectAsync(request);
    }
}
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

UnityアプリからセキュアにS3にアクセスするために


  • 自社でwebサービスを持っている場合は、Signed URLs or Signed Cookies for Amazon S3がよさそう感

Rubyでどう書けばよいかも聞いておく

require 'aws-sdk-s3'

class S3Controller < ApplicationController
  def create
    s3 = Aws::S3::Resource.new(region: 'us-west-2') # リージョンは適切なものに変更してください

    bucket = s3.bucket('your-bucket-name') # S3 バケット名を適切に設定してください
    object = bucket.object(params[:filename]) # ファイル名はリクエストパラメータから取得

    presigned_url = object.presigned_url(:put, acl: 'public-read', content_type: params[:content_type])
    
    render json: { url: presigned_url }
  end
end

一時的なURLの期限は?

presigned_url = object.presigned_url(:put, acl: 'public-read', content_type: params[:content_type], expires_in: 3600)
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

一時URLを使用したスクリプト

  • Webサービスなどの一時的なURLを発行するAPIを叩き、URLを取得する
  • そのURLを用いて、S3へアップロードする
  • これにより、UnityのクライアントがAWSのaccess token等を保持する必要がなくなる。また、AWS SDK for .NETも不要になる。
        private async void UploadAudioToS3()
        {
            var token = this.GetCancellationTokenOnDestroy();
            var presignedUrl = "https://一時的なURL";
            await _awsS3Uploader.UploadObjectAsync(_filePath, presignedUrl, token);
        }
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine.Networking;
using UnityEngine;
using System.IO;

public class AWSS3Uploader
{
    public async UniTask<bool> UploadObjectAsync(string filePath, string url, CancellationToken token)
    {
        byte[] bytes = await File.ReadAllBytesAsync(filePath, token);

        using (UnityWebRequest request = UnityWebRequest.Put(url, bytes))
        {
            await request.SendWebRequest().ToUniTask(cancellationToken: token);

            if (request.result == UnityWebRequest.Result.Success)
            {
                return true;
            }
            else
            {
                return false;
            }
        }
    }
}

だーら(Flamers / Memotia)だーら(Flamers / Memotia)

一時的なURLの有効期限

  • 有効期限(デフォルトでは15分に設定)されたURLの期限が切れた状態でアクセスすると、以下のようなエラーが発生する
UnityWebRequestException: HTTP/1.1 403 Forbidden
<?xml version="1.0" encoding="UTF-8"?>
<Error><Code>AccessDenied</Code><Message>Request has expired</Message>...
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

Ruby側で実装したコード

class S3Service
  REGION = "xxxxx"
  BUCKET_NAME = "xxxxx"

  def self.get_presigned_url(object_key)
    s3_bucket.object(object_key).presigned_url(:put)
  end

  private

  def self.s3_client
    Aws::S3::Client.new(
      region: REGION,
      access_key_id: ENV['AWS_ACCESS_KEY_ID'],
      secret_access_key: ENV['AWS_SECRET_ACCESS_KEY']
    )
  end

  def self.s3_bucket
    Aws::S3::Bucket.new(
      name: BUCKET_NAME,
      client: s3_client
    )
  end
end