「iOS」カテゴリーアーカイブ

【swift】An AVPlayerItem cannot be associated with more than one instance of AVPlayer

【macOS High Sierra 10.13.5+Xcode 9.3+iPhoneSE(iOS 11.1.2)】

カメラロールからビデオを検索して、AVPlayerで再生するデモを作っていて、ハマったのでメモ。

ビデオの再生して、もう一度同じビデオを再生しようとするとエラーになってアプリが落ちてしまうという現象に遭遇。表示されるエラーがこちら。

'NSInvalidArgumentException', reason: 'An AVPlayerItem cannot be associated with more than one instance of AVPlayer'

翻訳すると「AVPlayerItemを複数のAVPlayerインスタンスに関連付けることはできません」とのこと

何のことか理解できずにいろいろ試してみたところ、AVPlayerItemをそのままAVPlayerに使ってはいけないらしい。

・エラーになったコード(AVPlayerItemをそのままAVPlayerに渡す)

    var avPlayerItem: AVPlayerItem!
    var player:AVPlayer!
    var playerItem:AVPlayerItem?
    var playerController:AVPlayerViewController?

    func initItem(item:AVPlayerItem)
    {
        playerItem = item
        player = AVPlayer(playerItem:playerItem)
        playerController = AVPlayerViewController()
        playerController?.player = player
    }

・正しく動いたコード1(AVPlayerItemをurlで初期化して、AVPlayerに渡す)

    var avPlayerItem: AVPlayerItem!
    var player:AVPlayer!
    var playerItem:AVPlayerItem?
    var playerController:AVPlayerViewController?

    func initItem(item:AVPlayerItem)
    {
        playerItem = AVPlayerItem.init(url: (item.asset.value(forKey: "URL") as! NSURL) as URL)
        player = AVPlayer(playerItem:playerItem)
        playerController = AVPlayerViewController()
        playerController?.player = player
    }

・正しく動いたコード2(AVPlayerItemをassetで初期化して、AVPlayerに渡す)

    var avPlayerItem: AVPlayerItem!
    var player:AVPlayer!
    var playerItem:AVPlayerItem?
    var playerController:AVPlayerViewController?

    func initItem(item:AVPlayerItem)
    {
        playerItem = AVPlayerItem.init(asset: item.asset)
        player = AVPlayer(playerItem:playerItem)
        playerController = AVPlayerViewController()
        playerController?.player = player
    }

もう一つの初期化の方法として、

init(asset: AVAsset, automaticallyLoadedAssetKeys: [String]?)
Initializes a player item with the specified asset and the asset keys to be automatically

てのがあったんだけど、「automaticallyLoadedAssetKeys」がよくわからず割愛。

要するに「AVPlayerに使うAVPlayerItemは何らかの方法で初期化してから渡せ」と理解しました。勘違いがあったらご指摘いただけると喜びます。

【補足】
ちなみにカメラロールから動画を取得する処理はこんな感じ

var videos:NSMutableArray?

    //カメラロールから動画を読み込む
    func loadVideos() {
        
        //メディアタイプをビデオに絞って取得
        let assets:PHFetchResult = PHAsset.fetchAssets(with: PHAssetMediaType.video, options: nil)
        
        //取得したアセットを変換
        assets.enumerateObjects({(obj, index, stop) -> Void in
            //PHImageManagerを使ってplayerItemに
            self.phmov.requestPlayerItem(forVideo: assets[index], options: nil, resultHandler: {(playerItem, info) -> Void in
                //配列に追加
                videos!.add(playerItem!)
                //最後の処理が終わったらメインスレッドでコレクションビューをリロード
                if index == assets.count-1{
                    //終了時に呼び出す処理はココに書く
            })
        })
    }

ボタンが押されたら配列(videos)からAVPlayerItemを取りだして、再生するViewに移動

    @IBAction func tapBtn1(_ sender: UIButton) {
        gotoViewView(item: videos![0] as! AVPlayerItem)
    }

    func gotoViewView(item:AVPlayerItem) {
        
        let sb = UIStoryboard.init(name: "Main", bundle: nil)
        let vc = sb.instantiateViewController(withIdentifier: "videoVC") as! VideoViewController
        
        vc.initItem(item: item)
        self.present(vc, animated:false, completion:nil)
    }

【参考】
ios – 自作アルバムの動画をAVPlayerで再生するとsignal SIGABRT発生(iPad) – スタック・オーバーフロー

AVPlayerItem – AVFoundation | Apple Developer Documentation

【Swift】SwiftでAVPlayerの動画プレイヤーを作った – きみが思い出になる前に

Swift3で動画をカメラから選択してきて描画して再生まで

iOS 複数動画同時再生

AVPlayerの再生ステータス(playing/pause/end)を取得する – kitoko552.memo

【Swift3】UITextViewでテキストを省略表示する

テキストの省略表示(例:であるが、しかし…)をUITextViewでやりたかったけど、出てくるのはUILabelの話ばかり・・・。

で、ようやくわかりました。わかってみれば至極カンタン。

UITtextView.textContainer.lineBreakMode = .byTruncatingTail
UITtextView.textContainer.maximumNumberOfLines = 2;

「maximumNumberOfLines」は最大行数の指定で、上の指定だと2行目の最後が省略表示になります。指定しなければ、UITtextViewのサイズからはみ出す場合に省略表示がつきます。

一番最初のサイトが参考になりました。感謝。一番最後のコードで対応する方も試してみたけど、Universalで且つTableViewのcellで使いたかったのでボツに。

【参考】
UITextViewのわるいところ|まいちはシステム屋さん
byTruncatingTail – NSParagraphStyle.LineBreakMode | Apple Developer Documentation
[Swift3]長すぎる文字列の後方を省略する

【Xcode】各種ユーザーデータを初期処理する際の注意

【macOS Sierra 10.12.6+Xcode 8.3.3+iPhoneSE(iOS 10.3.2)】

とあるアプリで、カメラロールの写真とミュージックのアートワークを使ってたんですが、何故かインストール直後にどちらも0件になってしまう事象が発生。2回目以降に起動すると使えるのに・・・。

と思って調べたら、初期処理の実行タイミングに問題がありました。

※ここでいう「初期処理」とは、各種ライブラリをこのアプリ使うための実装処理を指します。

iOS10以降は各種ユーザーデータへアクセスに許可が必要になりました。で、以前書いたようにinfo.plistにメッセージ用テキストが必須になったわけですが、これで使用許可を求めるダイアログを表示してくれますが、該当処理はダイアログによる許可を待ってくれるわけではなく、許可されないと(ダイアログのボタンを押さないと)無視されてしまうようです。

で、viewDidLoad に書いてあった写真ライブラリとメディアライブラリの初期処理が、インストール直後でも正しく実行されるように、以下のように修正しました。

    //写真ライブラリ:初回許可(未設定の場合にダイアログの選択結果を受け取って、初期処理を行う)
    PHAuthorizationStatus status_photo = [PHPhotoLibrary authorizationStatus];
    if (status_photo == PHAuthorizationStatusNotDetermined){
        // このアプリに与える権限が未選択(アプリ初回起動時)
        NSLog(@"---PHPhotoLibrary:未選択");
        
        [PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) {
            if (status == PHAuthorizationStatusAuthorized) {
                // Access has been granted.
                NSLog(@"-----写真ライブラリ:使用OK");
                [self initIconList_photo];
            }
            else {
                // Access has been denied.
            }
        }];
        
    } else if (status_photo == PHAuthorizationStatusAuthorized){
        NSLog(@"-----写真ライブラリ:使用OK済み");
        [self initIconList_photo];
        
    }
    
    //メディアライブラリ設定:初回許可(未設定の場合にダイアログの選択結果を受け取って、初期処理を行う)
    MPMediaLibraryAuthorizationStatus status_media = [MPMediaLibrary authorizationStatus];
    if (status_media == MPMediaLibraryAuthorizationStatusNotDetermined){
        // このアプリに与える権限が未選択(アプリ初回起動時)
        NSLog(@"---MPMediaLibrary:未選択");
        
        [MPMediaLibrary requestAuthorization:^(MPMediaLibraryAuthorizationStatus status) {
            if (status == MPMediaLibraryAuthorizationStatusAuthorized) {
                // Access has been granted.
                NSLog(@"-----メディアライブラリ:使用OK");
                [self initIconList_artwork];
            }
            else {
                // Access has been denied.
            }
        }];
        
    } else if (status_media == MPMediaLibraryAuthorizationStatusAuthorized){
        NSLog(@"-----メディアライブラリ:使用OK済み");
        [self initIconList_artwork];
    }

要は、requestAuthorization でそれぞれのライブラリの使用許可ダイアログの結果を受け取って、OK(写真なら PHAuthorizationStatusAuthorized)だった場合に、初期処理(写真の場合は、initIconList_photo )を実行するということです。

ちなみに、else if で書かれているのは、既に許可済みの場合(写真だと PHAuthorizationStatusAuthorized)の場合に、初期処理を実行するためです(これがないと、後から許可した場合に初期処理が実行されない)。

各種ライブラリへのアクセス許可のダイアログは、info.plistにメッセージ用テキストを必須条件として、プログラム中で初めて該当ライブラリにアクセスしたタイミングで表示されるので、写真ライブラリとメディアライブラリのチェック順を入れ替えれば、ダイアログを出す順番も制御できます。また、特に初期処理が要らないライブラリなら、ユーザーがボタンを押したタイミングで許可を求めるダイアログを表示することもできそうです。

【参考】
iOSでカメラと写真の利用許可の確認方法[AVCaptureDevice] – Qiita
【Xcode】iOS10以降は各種ユーザーデータへアクセスに許可が必要 | AS blind side
【Xcode】iOS10で各種ユーザーデータへのアクセス許可を参照 | AS blind side
requestAuthorization: – MPMediaLibrary | Apple Developer Documentation

【Xcode】効果音と多重再生とSFSpeechRecognizer(音声認識)

作成中のアプリで使おうと思って調べたメモです。

サウンドの再生方法


調べてみると、下記の2つの方法があって、それぞれ用途によって総称されているフレームワークが違っている。

・AudioToolBoxを使う方法(効果音等短い音)
・AVFoundationを使う方法(BGM等長い音やループ)

【参考】
Objective-C:音楽(BGM)や効果音(SE)等のサウンド再生方法 | siro:chro
アプリ開発ブログ(仮): 効果音を鳴らす
【Xcode】アプリ内のボタンに効果音を付ける! | iDEACLOUD/dev

BGMと効果音を併用する際の問題点


効果音はAudioToolBox、BGMはAVFoundationで鳴らせばいいのかと思って、効果音をAudioToolBoxで組み込んで、テスト中にミュージックアプリで音楽流しながら、効果音を鳴らしたら音楽がフェードアウトしてしまった。で、調べてみたら、どうもAudioToolBoxとAVFoundationの併用に問題があるらしいことが発覚。

以下のリンクによれば、AudioToolBoxで音を鳴らすと、AVFoundationの方はフェードアウトしてしまうらしい。
なので、効果音もAVFoundationで再生することにする。

【参考】
BGMを鳴らしながら効果音再生するときの落とし穴 – iOSアプリ開発まとめWiki – アットウィキ

多重再生するための設定


更に調べてみると、多重再生させるには、以下の2つが必要とのこと。

・音声ファイルを .caf形式にする
・AVAudioSessionのカテゴリを変更する

.cafとは、Core Audio File の拡張子で、iOSおよびOS Xのネイティブのオーディオファイルフォーマットらしいです。

ターミナルから以下のコマンドで変換できます(例:sound_ok.m4a を sound_ok.caf に変換)

afconvert -f caff -d 0 sound_ok.m4a sound_ok.caf

※変換したところ、ファイルサイズは【107KB】>>【54KB】になりました。

また、AVAudioSessionのカテゴリは、デフォルトではオーディオ再生中にアプリを起動するとオーディオが停止(AVAudioSessionCategorySoloAmbient)するようになっているので、どちらも鳴らせるように設定を変更します。Objective-Cだと、AppDelegate.m にこんな感じに書きます。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // バックグラウンドでの音の再生を許可
    [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryAmbient error:nil];
    return YES;
}

【参考】
Swift:AVAudioSession アプリのバックグラウンドで音楽を再生する | siro:chro
iPhoneアプリ開発のためのサウンドフォーマットまとめ | ぱーくん plus idea
Core Audioの概要【PDF】
【iOS】音楽を止めずに効果音を同時に再生するには|てくめも@ecoop.net

SFSpeechRecognizer(音声認識)で効果音が再生されない問題


SFSpeechRecognizer(音声認識)を使ったアプリに効果音を組み込んでみたら、SFSpeechRecognizerを使って音声認識した後に、効果音が鳴らなくなった。

結果的には、SFSpeechRecognizerを使って音声認識を実行した後に、もう一度AVAudioSessionのカテゴリを変更してやることで解決しました。Objective-Cだと、こんな感じ。

// バックグラウンドでの音の再生を許可(AppDelegateで設定しているが、speechRecognizer使用後に再度設定しないと、音が鳴らない)
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryAmbient error:nil];

SFSpeechRecognizer自体はここでは触れないので、下記リンクを参照して下さい。要は、SFSpeechRecognizerを使う際にAVAudioSessionのカテゴリを録音用に変更(AVAudioSessionCategoryRecord)しているので、それを戻さないと音が鳴らないということです。

【参考】
[iOS 10] SFSpeechRecognizerで音声認識を試してみた | Developers.IO

【Xcode】iOS10で各種ユーザーデータへのアクセス許可を参照

【OSX 10.11.6+Xcode 8.0 (8A218a)+iPhoneSE(iOS 10.0.2)】

前回の記事でアプリの初回起動時以外に、各種ユーザーデータへのアクセス許可をどのように参照すればいいのかを前回対象とした「写真」「マイク」「音声認識」「メディアライブラリ」についてまとめました。

※以下、コードは Objective-C です。

写真


従来の写真アクセスである「ALAssetsLibrary」でコードを書いていると・・・

'ALAssetsLibrary' is deprecated: first deprecated in iOS 9.0 - Use PHPhotoLibrary from the Photos framework instead

というワーニングが出ます。

従来の「ALAssetsLibrary」はiOS9以降非推奨になり、Photos frameworkの「PHPhotoLibrary」を使いなさいということなので、「PHPhotoLibrary」ベースでのチェック方法です(もちろん写真のアクセス自体も「PHPhotoLibrary」ベースである前提です)。

//写真設定:参照(アプリの設定で許可されているかどうか)
-(BOOL)isPermitPhotoLibrary
{
    PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatus];
    switch (status) {
        case PHAuthorizationStatusNotDetermined:
            // このアプリに与える権限が未選択(アプリ初回起動時)
            return NO;
            break;
            
        case PHAuthorizationStatusRestricted:
            // 設定による使用制限で、アプリのアクセスの許可を変更できない
            return NO;
            break;
            
        case PHAuthorizationStatusDenied:
            // このアプリに与える権限を拒否(アプリ初回起動時「許可しない」を選択)
            return NO;
            break;
            
        default:
        case PHAuthorizationStatusAuthorized:
            // このアプリに与える権限を許可(アプリ初回起動時「OK」を選択)
            return YES;
            break;
    }
}

マイク


いくつか方法があるようですが、ここでは「AVAudioSessionRecordPermission」を使ってます。

//---マイク使用許可チェック
-(BOOL)isPermitMic{
    AVAudioSessionRecordPermission status = [AVAudioSession sharedInstance].recordPermission;
    
    switch (status) {
        case AVAudioSessionRecordPermissionUndetermined:
            // このアプリに与える権限が未選択(アプリ初回起動時)
            NSLog(@"---AVAudioSession:未選択");
            return NO;
            break;
            
        case AVAudioSessionRecordPermissionDenied:
            // このアプリに与える権限を拒否(アプリ初回起動時「許可しない」を選択)
            NSLog(@"---AVAudioSession:拒否");
            return NO;
            break;
            
        default:
        case AVAudioSessionRecordPermissionGranted:
            // このアプリに与える権限を許可(アプリ初回起動時「OK」を選択)
            NSLog(@"---AVAudioSession:許可");
            return YES;
            break;
    }
}

【参考】
マイクのアクセス許可 – iOS9とXcode7
authorizationStatusForMediaType: – AVCaptureDevice | Apple Developer Documentation
AVAudioSessionRecordPermission – AVFoundation | Apple Developer Documentation
Swift: アルバム・カメラ・マイク・プッシュ通知のアクセス許可判定一覧 | siro:chro

メディアライブラリ


MPMediaLibraryの「authorizationStatus」を使います。Appleのリファレンスにもあんまり詳しいことは書いてないです。何でだ?

//メディアライブラリ設定:参照(アプリの設定で許可されているかどうか)
-(BOOL)isPermitMediaLibrary
{
    MPMediaLibraryAuthorizationStatus status = [MPMediaLibrary authorizationStatus];
    switch (status) {
        case MPMediaLibraryAuthorizationStatusNotDetermined:
            // このアプリに与える権限が未選択(アプリ初回起動時)
            NSLog(@"---MPMediaLibrary:未選択");
            return NO;
            break;
            
        case MPMediaLibraryAuthorizationStatusRestricted:
            // 設定による使用制限で、アプリのアクセスの許可を変更できない
            NSLog(@"---MPMediaLibrary:許可を変更できない");
            return NO;
            break;
            
        case MPMediaLibraryAuthorizationStatusDenied:
            // このアプリに与える権限を拒否(アプリ初回起動時「許可しない」を選択)
             NSLog(@"---MPMediaLibrary:拒否");
            return NO;
            break;
            
        default:
        case MPMediaLibraryAuthorizationStatusAuthorized:
            // このアプリに与える権限を許可(アプリ初回起動時「OK」を選択)
            NSLog(@"---MPMediaLibrary:許可");
            return YES;
            break;
    }
}

【参考】
authorizationStatus – MPMediaLibrary | Apple Developer Documentation
MPMediaLibraryAuthorizationStatus – MediaPlayer | Apple Developer Documentation

音声認識(Speech.Framework)


SFSpeechRecognizerの「authorizationStatus」を使います。

    [SFSpeechRecognizer requestAuthorization:^(SFSpeechRecognizerAuthorizationStatus status) {
        dispatch_async(dispatch_get_main_queue(), ^{
            switch (status) {
                case SFSpeechRecognizerAuthorizationStatusAuthorized: {
                    NSLog(@"--許可あり");
                    break;
                }
                case SFSpeechRecognizerAuthorizationStatusDenied: {
                    NSLog(@"--拒否");
                    break;
                }
                case SFSpeechRecognizerAuthorizationStatusRestricted: {
                    NSLog(@"--設定により使えない");
                    break;
                }
                case SFSpeechRecognizerAuthorizationStatusNotDetermined: {
                    NSLog(@"--未認証");
                    break;
                }
            }
        });
        
    }];

【参考】
SFSpeechRecognizer – Speech | Apple Developer Documentation
authorizationStatus – SFSpeechRecognizer | Apple Developer Documentation
SFSpeechRecognizerAuthorizationStatus – Speech | Apple Developer Documentation

おまけ:自分のアプリの設定を呼び出す


各種ユーザーデータへのアクセス許可を参照して、許可がないものは改めて許可してもらいましょう。ということで、設定アプリの中にある自分の設定を呼び出す方法です。

いわゆるURLスキームというやつらしいですが、iOS10で使い方(openURLのメソッド)が変わっているようです。また下記の参考リンクに「URLスキームの内容をinfo.plistに記載する」とありますが、単純に自分のアプリ設定を呼び出すだけなら不要なようです(実機で動作確認済)。

//このアプリの設定を呼び出し
- (void)prefsButtonPushed {
    NSURL *url = [NSURL URLWithString:UIApplicationOpenSettingsURLString];
    if ([[UIApplication sharedApplication] canOpenURL:url]) {
        [[UIApplication sharedApplication] openURL:url
                                           options:@{}
                                 completionHandler:nil];
        //以下は、iOS9までの書き方
        //[[UIApplication sharedApplication] openURL:url];
    }
}

これを実行すると、自分のアプリの設定が表示されます。

自分のアプリの設定が表示されます。
自分のアプリの設定が表示されます。

【参考】
application:openURL:options: – UIApplicationDelegate | Apple Developer Documentation
UIApplicationOpenSettingsURLString – UIKit | Apple Developer Documentation
[iOS 10] UIApplication の openURL: が Deprecated になりました | Developers.IO
iOS9でカスタムURLスキームの遷移に失敗するときの注意点 – Qiita
【iOS9】特定のURLスキームのみを呼び出し可能にする→.canOpenURL(url)について | MUSHIKAGO APPS MEMO
投稿の編集 ‹ AS blind side — WordPress
標準アプリのURLスキームについて – もう一人のY君

最後に


今回出てきた「Photo.Framework」「Speech.Framwork」については、別途まとめたいですが、とりあえず気になる人は下記を参考に。

・Photo.Framework
Photos vs Assets Library – いまさら始めるPhotos.framework
[iOS 8] PhotoKit 1 – Photos frameworkの概要 | Developers.IO
Photos Framework を使って写真.appと画像をやりとりする – Qiita
Photos frameworkを使ってiPhoneアルバム内の写真を取得・削除する+α – Think Big Act Local
Photos.framework でカメラロールを取得する – ObjecTips
投稿の編集 ‹ AS blind side — WordPress

・Speech.Framwork
[iOS 10] SFSpeechRecognizerで音声認識を試してみた | Developers.IO
[iOS 10] SFSpeechRecognizerの音声認識処理の仕組みを見てみる | Developers.IO
【iOS 10】Speechフレームワークで音声認識 – 対応言語リスト付き – Over&Out その後
新規投稿を追加 ‹ AS blind side — WordPress
【iOS】Speech Frameworkの実装 – Qiita
【iOS 10】Speech Frameworkで音声認識 – あたも開発ブログ
【iOS10】Speech Recognition API(音声認識API)の制約まとめ – Qiita

【Xcode】iOS10以降は各種ユーザーデータへアクセスに許可が必要

【OSX 10.11.6+Xcode 8.0 (8A218a)+iPhoneSE(iOS 10.0.2)】

iPhoneの音楽ライブラリからアートワークを使おうと思って、前に書いた古いコードから必要な部分を移植して動かしてみたらエラーが出た。

[access] This app has crashed because it attempted to access privacy-sensitive data without a usage description.  The app's Info.plist must contain an NSAppleMusicUsageDescription key with a string value explaining to the user how the app uses this data.

iOS10以降は、ユーザーデータにアクセするには、Info.plistに該当アクセスに対応したキーと使用目的を示すテキストを記述する必要があるようです。詳しくは下記リンクを。

[iOS 10] 各種ユーザーデータへアクセスする目的を記述することが必須になるようです | Developers.IO

具体的には、TARGETSからinfoを選んで、下記のように追加するだけ。

TARGETS > info で必要なアクセス先に対する許可を追加します。
TARGETS > info で必要なアクセス先に対する許可を追加します。ここではメディアライブラリ、マイク、音声解析、写真ライブラリの4つを指定。

Keyの選択肢は、全て「Privacy – 」で始まっているので、そこから必要なものを追加すると、アプリの初回起動時にそれぞれ許可を求めるダイアログが表示されます(keyに指定したString部分が、ダイアログの説明分として表示されます)。

info.plistで設定したアクセスに対して、許可を求めるダイアログが表示される。
info.plistで設定したアクセスに対して、許可を求めるダイアログが表示される。

ダイアログでの選択(許可するかしないか)は、iPhoneの 設定>アプリ名の中に保存され、ユーザーが切り替え可能です。逆に言うと、初回起動時以降は、ユーザーに設定で許可を変更するよう促す必要がある、ということですね。

アクセス許可は、iPhoneの 設定>アプリ名の中に保存される。
アクセス許可は、iPhoneの 設定>アプリ名の中に保存される。

で、冒頭の音楽ライブラリのアートワークを使うには、メディアライブラリへのアクセス許可を求めればいいようです(今回は音楽データそのものは使っていません)。

【参考】
iOS10ではカメラアクセスなどの目的を明示しないと強制終了する – Qiita
[iOS 10] ユーザーのプライバシー情報にアクセスするときは理由を書こう | Developers.IO
iOS 10 からユーザの許可を求めるアラートへの説明文追加が必須に – 強火で進め