月別アーカイブ: 2013年5月

AVFoundationを利用した動画表示用の便利クラス (使い方)

前回UPした動画再生クラス MovieView の使い方です
今回はサンプルとしてスライダーで再生位置の調整も行えるようにしてみました。

プライベートな変数にこんなのを用意して

UISlider* _slider;
MovieView* _movieView;

ビューの生成と破棄


-(void)dealloc
{
    if(_movieView){
        _movieView.delegate = nil;
        [_movieView removeFromSuperview];
        _movieView = nil;
    }
       
    [super dealloc];
}


- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {

     //スライダーの生成
     _slider = [[[UISlider alloc]initWithFrame:CGRectMake(0, 0, 300, 40)]autorelease];
        [self addSubview:_slider];
        [_slider addTarget:self action:@selector(seekBarValueChanged:) forControlEvents:UIControlEventValueChanged];

     //動画再生用Viewの生成
     _movieView = [[[MovieView alloc]initWithFrame:self.bounds]autorelease];
        _movieView.delegate = self;     //※再生だけならdelegateの設定は不要ですよ。
        [self addSubview:_movieView];
       
     //リソースからムービーのURLを取得して
        NSURL *url = [[NSBundle mainBundle] URLForResource:@"opening" withExtension:@"mov"];

     //再生開始
     [_movieView playMovie: url];
    }
    return self;
}

各種イベントのハンドリング


//スライダーからのコールバック
- (void)seekBarValueChanged:(UISlider *)slider
{    
    //ムービーのシーク
    float val = slider.value;
    float duration = _movieView.movieDuration;
    float time = val * duration / _slider.maximumValue;

    [_movieView seekToSeconds:time];   
}


#pragma mark - MovieViewDelegate

//再生開始時の処理
-(void)movieView:(MovieView*)sender movieWillPlayItem:(AVPlayerItem*)playerItem duration:(Float64)duration
{
    //スライダーを初期位置に変更
    _slider.minimumValue = 0;
    _slider.maximumValue = duration;
    _slider.value        = 0;
   
}

//再生が進んだ時に順次呼ばれる
-(void)movieView:(MovieView*)sender moviePlayAtTime:(Float64)time duration:(Float64)duration
{
    //スライダーの位置を再生時間に合わせる
    float value = _slider.maximumValue * time / duration;
    _slider.value = value;
}

 
以上のような感じです。
スライダー関連の処理が多くなってしまいましたが、単純に再生するだけなら

_movieView = [[[MovieView alloc]initWithFrame:self.bounds]autorelease];
[self addSubview:_movieView];

NSURL *url = [[NSBundle mainBundle] URLForResource:@"opening" withExtension:@"mov"];
[_movieView playMovie: url];

これだけでいけます。

AVFoundationを利用した動画表示用の便利クラス

チュートリアルの一部やオープニング画面など、アプリ内で部分的にムービーを表示したい事ありますよね。

MPMoviePlayerControllerを使えば、至れり尽くせりで超簡単!なんですけど、UIを自分で作ったり細かい制御をしたい場合にはやはりAVFoundationを使った方が良さそうです。
AVFoundationを使ってもそんなに難しい処理ではないんですけど、表示アイテムの管理や各種Observerの管理などでちょっと面倒。
そんな時には下記のクラスを使って下さい。
UI無しで指定された動画の再生だけを行います。
再生・一時停止・シークなどはデレゲート、各メソッドを通じて外のUIを使うように作ってあります。

事前に AVFoundation.framework、CoreMedia.framework へのリンクを追加しておいて下さいね。

ヘッダー部から


#import <UIKit/UIKit.h>
#import <AVFoundation/AVFoundation.h>

@protocol MovieViewDelegate;

/**
 * 動画再生をView
 */
@interface MovieView : UIView
{

}

@property (nonatomic,assign) id<MovieViewDelegate> delegate;

//リピート再生するか
@property (nonatomic,assign) BOOL repeat;

//動画の長さ(秒数)
@property (nonatomic,readonly) Float64 movieDuration;

//再生を開始する
-(void)playMovie:(NSURL*)url;

//再生の一時停止
-(void)pauseMovie;

//再生開始
-(void)playMovie;

//再生位置を移動させる
-(void)seekToSeconds:(Float64)seconds;

//ムービーのクリア
- (void)clear;


@end


@protocol MovieViewDelegate <NSObject>

//動画の再生直前に呼び出される
-(void)movieView:(MovieView*)sender movieWillPlayItem:(AVPlayerItem*)playerItem duration:(Float64)duration;

//動画の再生中に一定の間隔で呼び出される
// time:再生位置(秒数)
// duration:動画全体の秒数
-(void)movieView:(MovieView*)sender moviePlayAtTime:(Float64)time duration:(Float64)duration;

@end


次に実装部はこんな感じ


#import "MovieView.h"


#define TIME_OVSERVER_INTERVAL  0.25f

@interface MovieView()
{   
    bool _is_playing;     //ムービーが再生中である事を示す
}

@property (nonatomic,retain) AVPlayerItem* playerItem;
@property (nonatomic,retain) AVPlayer*     player;
@property (nonatomic,assign) id  playTimeObserver;

@end

@implementation MovieView

#pragma mark -
#pragma mark class method

//自身のレイヤーを動画再生用レイヤーを返すようにオーバライド
+ (Class)layerClass
{
    return [AVPlayerLayer class];
}


#pragma mark -
#pragma mark instance management

-(void)dealloc
{
    _delegate = nil;
       
    [self clear];
   
    [_playerItem release];
    [_player release];

    [super dealloc];
}

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        // Initialization code
        _is_playing = false;
        _repeat = TRUE;
        self.backgroundColor = [UIColor clearColor];
    }
    return self;
}

#pragma mark property method

-(Float64)movieDuration
{
    if (self.playerItem)
    {
        Float64 duration = CMTimeGetSeconds( [self.player.currentItem duration] );
        return duration;
    } else {
        return 0;
    }
}



#pragma mark -
#pragma mark movie control

-(void)playMovie:(NSURL*)url
{
    //再生用アイテムを生成
    if (_playerItem)
    {
        //前回追加したムービー終了の通知を外す
        [[NSNotificationCenter defaultCenter] removeObserver:self
                                                        name:AVPlayerItemDidPlayToEndTimeNotification
                                                      object:self.playerItem];
       
        self.playerItem = nil;
    }
    self.playerItem = [[[AVPlayerItem alloc] initWithURL:url] autorelease];
   

    //Itemにムービー終了の通知を設定
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(playerDidPlayToEndTime:)
                                                 name:AVPlayerItemDidPlayToEndTimeNotification
                                               object:self.playerItem];
   
   
    Float64 movieDuration = CMTimeGetSeconds( self.playerItem.duration );
   
    if (self.player)
    {
               
        //アイテムの切り替え
        [self.player replaceCurrentItemWithPlayerItem:self.playerItem];
       
        [_delegate movieView:self movieWillPlayItem:self.playerItem duration:movieDuration];
       
    } else {

        //AVPlayerを生成
        self.player = [[[AVPlayer alloc] initWithPlayerItem:self.playerItem] autorelease];
        AVPlayerLayer* layer = ( AVPlayerLayer* )self.layer;
        layer.videoGravity = AVLayerVideoGravityResizeAspect;
        layer.player       = self.player;
       
        //delegate呼び出し
        [_delegate movieView:self movieWillPlayItem:self.playerItem duration:movieDuration];
       
        // 再生時間とシークバー位置を連動させるためのタイマーを設定
        __block MovieView* weakSelf = self;
        const CMTime intervaltime     = CMTimeMakeWithSeconds( TIME_OVSERVER_INTERVAL, NSEC_PER_SEC );
        self.playTimeObserver = [self.player addPeriodicTimeObserverForInterval:intervaltime
                                                                               queue:NULL
                                                                          usingBlock:^( CMTime time ) {
                                                                              //再生時になにか動作させるのであればここで。
                                                                              [weakSelf moviePlaying:time];
                                                                          }];
    }
   
    [self playMovie];
}




-(void)playMovie
{
    if (self.player)
    {
        [self.player play];
        _is_playing = true;
    }
}

-(void)pauseMovie
{   
    if (_is_playing)
    {
        [self.player pause];
        _is_playing = false;
    }
   
}

-(void)seekToSeconds:(Float64)seconds
{
    if (self.player)
    {
        [self.player seekToTime:CMTimeMakeWithSeconds(seconds, NSEC_PER_SEC )];
    }
}



//再生中のムービーを停止し、プレイヤーなどをクリアする
- (void)clear
{
    if (self.player)
    {
        [self.player removeTimeObserver:self.playTimeObserver];
        [self.player pause];
        [[NSNotificationCenter defaultCenter] removeObserver:self
          name:AVPlayerItemDidPlayToEndTimeNotification object:self.playerItem];
       
        AVPlayerLayer* layer = ( AVPlayerLayer* )self.layer;
        layer.player  = nil;
       
        self.player = nil;
        self.playerItem = nil;
        self.playTimeObserver = nil;       
    }
       
}

#pragma mark -
#pragma mark movie events

// TIME_OVSERVER_INTERVALで指定された間隔で実行される
-(void)moviePlaying:(CMTime)time
{
    Float64 duration = CMTimeGetSeconds( [self.player.currentItem duration] );
    Float64 time1    = CMTimeGetSeconds(time);
   
    if (_delegate){
   
        [_delegate movieView:self moviePlayAtTime:time1 duration:duration];
   
    }
   
}

// ムービー完了時に実行される
- (void) playerDidPlayToEndTime:(NSNotification*)notfication
{

    if (self.player)
    {
        if (self.repeat)
        {
            //リピート再生
            [self.player seekToTime:kCMTimeZero];
            [self.player play];
        }
    }
}


@end

使い方のサンプルは長くなったので次回へ持ち越し

UIScrollViewを一方向だけにしかスクロールを許さない方法

縦だけとか横だけのサンプルは良く見かけるのですが、例えば上スクロールだけを許して下にはスクロールさせない方法が見つからなかったので書いておきます。
(こんな制御に需要があるとはあまり思えませんが。。)

ポイント1 スクロール方向の判定

UIScrollViewDelegateのscrollViewWillBeginDragging、scrollViewDidScrollでスクロールの前回座標を保存し、
scrollViewDidScrollでスクロールされた後の座標と比較すればOK

ポイント2 スクロールのキャンセル

スクロールの方向を求め、許可する方向以外だった時はスクロールの位置をスクロールの開始座標に戻します。
また、座標を戻したとしても慣性スクロールが発生しますのでscrollViewWillBeginDeceleratingで慣性スクロールをキャンセルします。(以前の記事参照

実装は下記みたいな感じです

UIScrollViewDelegateを宣言に追加しておいて下さいね

//インスタンス変数としてどこかに宣言してください
CGPoint _scrollPrevPoint;  //スクロールの開始位置
BOOL _cancelDecelerating;  //慣性スクロールをキャンセルするフラグ    
int _scrolling_direction;  //0:未確定 1:上(offset.yが小さくなる) 2:下(offset.yが大きくなる)


#pragma mark - UIScrollViewDelegate
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{

    _scrolling_direction = 0;
    _cancelDecelerating = false;
    _scrollPrevPoint = [scrollView contentOffset];
}


-(void)scrollViewDidScroll:(UIScrollView *)scrollView
{
    CGPoint currentPoint = [scrollView contentOffset];
   
    if (CGPointEqualToPoint(_scrollPrevPoint, currentPoint)){
        return;
    }
    else {
     //スクロール方向の判定
        _scrolling_direction = (_scrollPrevPoint.y < currentPoint.y) ? 2 : 1;
    }
   
    //下スクロールのキャンセル
    if (_scrolling_direction == 2)
    {    
        currentPoint.y = _scrollPrevPoint.y;
        [scrollView setContentOffset:currentPoint];
        _cancelDecelerating = true; //慣性スクロールを止めるためのフラグをセット
    }
}

- (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView
{
    //慣性スクロールを止める
    if (_cancelDecelerating){
        [scrollView setContentOffset:scrollView.contentOffset animated:NO];
    }
}

上記のような実装を行ったインスタンスをUIScrollViewのdelegateに指定してあげればOK

以上

AudioServicesを使って効果音を鳴らす便利クラス

皆さん効果音鳴らしてますかー?
私はアプリの効果音はぜんぶ消して使っちゃう派なんですけど、
世の中には効果音が無いと操作したか分かんないよ派もいるんですよね。

さて、そんな他派の為に効果音を鳴らすんですけど、iOSには色々サウンドのAPIがあってどれを使えば良いのか。。
5秒以下のサウンドに限定されますが、とりあえず簡単に使えそうなAudioServicesを使ってみます。

基本的には
1.SystemSoundID を取得

SystemSoundID soundIdRing;
NSURL* url = [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"ring" ofType:@"wav"]];
AudioServicesCreateSystemSoundID((CFURLRef)url, &soundIdRing);

2.AudioServicesPlaySystemSound を呼ぶ事によりサウンドを再生

AudioServicesPlaySystemSound(soundIdRing); 

3.使い終わったら AudioServicesDisposeSystemSoundID でメモリの解放

AudioServicesDisposeSystemSoundID(soundIdRing); 

でOK
こんなお手軽APIなのですが、効果音ってのは数種類あるものなので管理が面倒です。
なので、私は下記のようなクラスで使ってます。

ヘッダー部


#import <Foundation/Foundation.h>
#import <AudioToolbox/AudioServices.h>

@interface SoundManager : NSObject

-(void)addSoundEffect:(int)sound_no url:(NSURL*)url;
-(void)playSoundEffect:(int)sound_no;

@end

実装部


#import "SoundManager.h"

@interfaceSoundManager ()

//登録されたサウンドのSystemSoundIDを保存する
@property (nonatomic,retain) NSMutableDictionary* sound_dir;

@end

@implementation SoundManager

-(id)init
{
    self = [super init];
    if (self)
    {
        self.sound_dir = [[[NSMutableDictionary alloc]init]autorelease];    
    }
    return self;
}

-(void)dealloc
{
    if (_sound_dir)
    {
        for(NSNumber* box in _sound_dir)
        {
            SystemSoundID soundid = box.unsignedLongValue;
            AudioServicesDisposeSystemSoundID (soundid);
        }
        
        [_sound_dir removeAllObjects];
    }
        
    [_sound_dir release];

    [super dealloc];
}


-(void)addSoundEffect:(int)sound_no url:(NSURL*)url
{
    SystemSoundID soundid;
    
    AudioServicesCreateSystemSoundID ((CFURLRef)url, &soundid);
    
    NSNumber *box = [NSNumber numberWithUnsignedLong:soundid];
    
    [self.sound_dir setObject:box forKey:[NSString stringWithFormat:@"%d",sound_no]];
    
}


-(void)playSoundEffect:(int)sound_no
{
    NSNumber* box = [self.sound_dir objectForKey:[NSString stringWithFormat:@"%d",sound_no]];
    if (box)
    {
        SystemSoundID soundid = box.unsignedLongValue;
        AudioServicesPlaySystemSound (soundid);
    }
}

使い方はこんな感じ


//インスタンスを準備して
self.soundManager = [[[SoundManager alloc]init]autorelease];


//効果音の登録
NSURL *url;
url = [[NSBundle mainBundle] URLForResource:@"ring" withExtension:@"wav"];
[soundManager addSoundEffect:1 url:url];

 url = [[NSBundle mainBundle] URLForResource:@"bang" withExtension:@"wav"];
[soundManager addSoundEffect:2 url:url];


//再生
[self.soundManager playSoundEffect:1];

1とか2とか指定している部分はenumにするといいですよ。

以上

UIImagePickerControllerで動画だけを選択する方法

UIImagePickerControllerをそのまま使用すると静止画を選択できますが、動画が選択できません。
今回は動画を選択する方法です。

ポイントは
 mediaTypesにkUTTypeMovieを設定する
 UIImagePickerControllerMediaURLを使って動画のURLを取得する
だけなんですけどね。

以下実例

事前にのインポートとUINavigationControllerDelegateとUIImagePickerControllerDelegateを対応プロトコルに追加しておきます。

UIImagePickerControllerを呼び出す時はこんな感じ
ちなみにkUTTypeMovieの中身は”public.movie”です。


if([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypePhotoLibrary])
{
     UIImagePickerController* picker = [[[UIImagePickerController alloc]init]autorelease];
     picker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
     picker.mediaTypes =  @[ (NSString*)kUTTypeMovie ];
     picker.allowsEditing = NO;
     picker.delegate = self;
                
     [self presentViewController: picker animated:YES completion:nil];//selfはUIViewController
}
else {
   //エラー処理
}

UIImagePickerControllerで選択されたときの処理はこんな感じ

#pragma mark - UIImagePickerControllerDelegate

-(void)imagePickerController:(UIImagePickerController*)picker
  didFinishPickingMediaWithInfo:(NSDictionary *)info
{
   
    NSString* mediaType = [info objectForKey:UIImagePickerControllerMediaType];
    if ([mediaType isEqualToString:(NSString*)kUTTypeMovie])
    {
        //動画のURLを取得
        NSURL* url = [info objectForKey:UIImagePickerControllerMediaURL];
         
        [self playMovie:url];//何かしら処理
    }
   
   
    [self dismissViewControllerAnimated:YES completion:nil];
}


-(void)imagePickerControllerDidCancel:(UIImagePickerController*)picker
{
    [self dismissViewControllerAnimated:YES completion:nil];
}

以上

セレクタの呼び出し方

セレクタの呼び出し方をまとめました。

方法1 performSelectorを使う

簡単に呼び出せるのでおすすめですが、引数が2つまでのセレクタしか呼び出せません。

id _target = self;
SEL _selector = @selector(hogemethod:);

int tag = self.tag;
NSObject* retVal = [_target performSelector: _selector withObject:tag];
方法2 NSInvocationを使う

汎用的で攻守最強??でも冗長で面倒ですね。

NSMethodSignature* signature = [[_target class] instanceMethodSignatureForSelector: _selector];
if (signature)
{
     NSInvocation* invocation = [NSInvocation invocationWithMethodSignature:signature];
     [invocation setTarget: _target];
     [invocation setSelector: _selector];

     //引数の設定 第1引数はindex=2となる
     [invocation setArgument:&tag atIndex:2];
       
     //セレクタの実行
     [invocation invoke];
       
     //戻り値を取得
     NSObject* retVal;
     [invocation getReturnValue:&retVal];      
}

他にも objc_msgSend を使う方法があるようですが、私は使った事がありません。