アカベコマイリ

HEAR NOTHING SEE NOTHING SAY NOTHING

iOS の動画再生を試す

iOS アプリで動画を再生しくなったので方法を調べてみた。標準の動画再生 API としは MPMoviePlayerController と AVPlayer の 2 種類が提供されている。そのため両方を利用したサンプルを実装してみる。

再生対象となる動画の選択

はじめに再生対象とする動画の選択方法について考える。

最も簡単なのはアプリ内のリソースとして動画ファイルを組み込む方法である。しかし静止画に比べ動画はかなり大きいため、アプリのサイズに影響する。再生対象が固定になる点もイマイチ。そこで今回はカメラロールから動画を選択するようにしてみる。先月書いた iOS でグリッド表示という記事で作成したサンプル プログラムを元に動画だけ選択する画面を実装。

動画の選択

今回のサンプルでは対象とするグループを ALAssetsGroupSavedPhotos に限定して ALAssetsGroup でコンテンツを列挙した時も動画のみを追加するようにしている。

/**
 * アセット情報を読み込みます。
 */
- (void)loadAssets
{
    [self.assetsGroup enumerateAssetsUsingBlock:^(ALAsset *result, NSUInteger index, BOOL *stop)
    {
        if( result != nil )
        {
            // 動画のみ追加
            NSString* type = [result valueForProperty:ALAssetPropertyType];
            if( [type isEqualToString:ALAssetTypeVideo] )
            {
                [self.assets addObject:result];
            }
        }

        // 列挙完了
        if( index + 1 == self.assetsGroup.numberOfAssets )
        {
            [self performSelectorOnMainThread:@selector(reloadGridView) withObject:nil waitUntilDone:NO];
        }
    }];
}

/**
 * アセット情報のグループを読み込みます。
 */
- (void)loadAssetsGroups
{
    [self.assetsLibrary enumerateGroupsWithTypes:ALAssetsGroupSavedPhotos usingBlock:^( ALAssetsGroup *group, BOOL *stop )
    {
        if( group == nil ) { return; }

        *stop            = YES;
        self.assetsGroup = group;
        self.assets      = [NSMutableArray arrayWithCapacity:self.assetsGroup.numberOfAssets];
        [self loadAssets];
    }
    failureBlock:^( NSError *error )
    {

    }];
}

iOS シミュレーター用の動画登録

カメラロールから画像を選択できるようになったので、次は iOS シミュレーター用に動画登録する機能を実装してみる。

iOS 端末の実機ならカメラで撮影した動画を選択すればよいのだが iOS シミュレーターの場合、カメラが利用できない。そのためアプリの Documents フォルダ内に動画ファイルを置き、それをプログラムから登録する方法で対応してみた。

  • 余談
    • 画像であれば、iOS シミュレーターに転送するための方法がある
    • 画像ファイルを Mac の Finder から iOS シミュレーターにドラッグ & ドロップすることで Safari が開く
    • そして Safari 上の画像をロング タップすれば、カメラロールに保存するためのメニューが表示される

iOS シミュレーターのアプリ内 Documents フォルダは Finder からアクセス可能。例えば iOS 6.0 シミュレーターなら以下の位置になる。

/Users/ユーザー名/Library/Application Support/iPhone Simulator/6.0/Applications/アプリの ID/Documents

この中に動画ファイルを配置すれば iOS シミュレーター上で動作するアプリからもアクセスできる。よってまずは適当な動画 (MP4 など) を探して入れておく。

iOS シミュレーターの Documents

次に動画をカメラロールに保存する処理を実装。

NSArray*  paths = NSSearchPathForDirectoriesInDomains( NSDocumentDirectory, NSUserDomainMask, YES );
NSString* dir   = [paths objectAtIndex:0];
NSArray*  files = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:dir error:nil];

NSInteger savedCount = 0;
for( NSString* file in files )
{
    NSString* extention = [file pathExtension];
    if( [extention isEqualToString:@"mov"] ||
        [extention isEqualToString:@"mp4"] ||
        [extention isEqualToString:@"m4v"])
    {
        NSString* path = [dir stringByAppendingPathComponent:file];
        UISaveVideoAtPathToSavedPhotosAlbum( path, nil, nil, nil );
        savedCount++;
    }
}

Documents フォルダ内から動画ファイルを列挙し UISaveVideoAtPathToSavedPhotosAlbum でカメラロールに保存。サンプルのメニュー画面から My video to album を選択すると保存が実行される。終了後、保存した動画の数をアラートで表示している。

動画をカメラロールに登録

保存されるコンテンツは重複チェックされないらしく、この処理を実行するたびにカメラロールへ動画が追加されるので注意する。よって一度でも登録した動画は Finder から消しておいたほうがいい。

フルスクリーン

動画プレーヤーとなる画面はフルスクリーンにしたいので MPMoviePlayerControllerAVPlayer それぞれのデモ画面で以下の処理を実行している。

/**
 * 画面が表示される時に発生します。
 *
 * @param animated 表示アニメーションを有効にする場合は YES。それ以外は NO。
 */
- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];

    [self setWantsFullScreenLayout:YES];
    [[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleBlackTranslucent];
    [self.navigationController.navigationBar setBarStyle:UIBarStyleBlackTranslucent];
    [self.navigationController.navigationBar setTranslucent:YES];
    [self.navigationController.view setNeedsLayout];
}

/**
 * 他の画面へ切り替わる時に発生します。
 *
 * @param animated 表示アニメーションを有効にする場合は YES。それ以外は NO。
 */
- (void)viewWillDisappear:(BOOL)animated
{
    [[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleBlackOpaque];
    [self.navigationController.navigationBar setBarStyle:UIBarStyleBlackOpaque];
    [self.navigationController.navigationBar setTranslucent:NO];

    [super viewWillDisappear:animated];
}

画面が表示されるときにステータス バーとナビゲーション バーを透過して再レイアウト指定。画面が切り替わるときは前後の画面にあった状態へ戻している。

もっとスマートに処理したいけどうまい方法が見つからなかった。例えば iOS が前の画面のステータス バーとナビゲーション バーの状態を記憶していて API 一発でリストアしてくれたりすると嬉しいのだが、そういう方法はないものだろうか。

MPMoviePlayerController

iOS で手軽に動画再生したいなら MPMoviePlayerController がよさそうだ。このコントロールを利用する場合はアプリの Xcode プロジェクトに MediaPlayer.framework を追加する必要がある。

MPMoviePlayerController には iOS の標準動画プレーヤー並みの機能と UI が用意されている。そのまま利用するなら必要となる実装はコントロールの設置と動画の指定ぐらいだろう。

/**
 * 画面が読み込まれる時に発生します。
 */
- (void)viewDidLoad
{
    [super viewDidLoad];

    self.title = @"MPMoviePlayerController";

    self.videoPlayer = [[[MPMoviePlayerController alloc] initWithContentURL:self.videoUrl] autorelease];
    self.videoPlayer.controlStyle             = MPMovieControlStyleDefault;
    self.videoPlayer.scalingMode              = MPMovieScalingModeAspectFit;
    self.videoPlayer.shouldAutoplay           = NO;
    self.videoPlayer.view.autoresizingMask    = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
    self.videoPlayer.view.autoresizesSubviews = YES;
    self.videoPlayer.view.frame               = self.view.bounds;

    [self.videoPlayer prepareToPlay];
    [self.view addSubview:self.videoPlayer.view];
}

表示してみる。

MPMoviePlayerController 通常

再生パネル (と呼ぶのだろうか?) 右端に置かれた矢印のボタンをタップするとフルスクリーンに移行。このときステータス バーとナビゲーション バーは自動的に隠される。

MPMoviePlayerController フルスクリーン

再生コントロールを自前で用意したい場合は controlStyle プロパティに MPMovieControlStyleNone を指定して play/pause/stop メソッドなどを制御することになる。しかしその目的なら自由度の高い AVPlayer のほうが向く。

AVPlayer

純粋な動画再生 & 表示機能だけ欲しくて UI を自作するつもりなら AVPlayer がよい。このクラスを利用する場合はプロジェクトに AVFoundation.framework を追加する。また AVPlayer 関連では CMTimer などを操作をする機会が多いので CoreMedia.framework を追加してユーティリティ関数を使用できるようにしておく。

UI

AVPlayer は操作用コントロールを一切、用意していないため自作が必要となる。とりあえず今回は以下の構成で実装することにした。

AVPlayer

画面下部がコントロールとなる。左から

  • 再生・一時停止ボタン
  • 再生時間
  • 再生位置スライダー
  • 演奏時間

となる。

AVPlayerView

AVPlayer は単なる動画プレーヤーなので表示には UIViewAVPlayerLayer が要る。Apple のサンプル プロジェクトなどでは AVPlayerLayer をホストする専用の UIView を用意しているようだ。今回はそれを踏襲。

UIView 派生の AVPlayerView クラスを用意する。

#import <UIKit/UIKit.h>

/**
 * AVPlayer によって再生された動画を表示します。
 */
@interface AVPlayerView : UIView

@end

ヘッダーでは特にプロパティやメソッドは追加しない。

#import "AVPlayerView.h"
#import <AVFoundation/AVFoundation.h>

@implementation AVPlayerView

/**
 * レイヤーのクラス情報を取得します。
 *
 * @return レイヤー。
 */
+ (Class)layerClass
{
    return [AVPlayerLayer class];
}

@end

重要なのは layerClass メソッドをオーバーライドて AVPlayerLayer を返す処理。これで AVPlayerViewAVPlayerLayer をホストするようになる。初見では UIView.layer - addSublayer すればよいのでは?と思ったのだが、それでは正しく動作しないらしい。この実装が必須だとしたら AVPlayer 関連の処理も AVPlayerView に局所化したほうがよいのかもしれない。

AVPlayer の初期化

AVPlayerView を実装したので AVPlayer を組み合わせて動画再生してみる。まず UIViewControllerAVPlayerView が配置されているとした場合、初期化は以下のようになる。

// 以下が定義されているものとする。
// AVPlayerView* videoPlayerView
// AVPlayerItem* playerItem
// AVPlayer*     videoPlayer

self.playerItem = [[[AVPlayerItem alloc] initWithURL:self.videoUrl] autorelease];
[self.playerItem addObserver:self
                  forKeyPath:kStatusKey
                     options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew
                     context:AVPlayerViewControllerStatusObservationContext];

// 終了通知
[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(playerDidPlayToEndTime:)
                                                name:AVPlayerItemDidPlayToEndTimeNotification
                                              object:self.playerItem];

self.videoPlayer = [[[AVPlayer alloc] initWithPlayerItem:self.playerItem] autorelease];

AVPlayerLayer* layer = ( AVPlayerLayer* )self.videoPlayerView.layer;
layer.videoGravity = AVLayerVideoGravityResizeAspect;
layer.player       = self.videoPlayer;

はじめに再生対象となる AVPlayerItem を生成。これはコンテンツ情報そのものを表すオブジェクトで演奏時間なども管理している。

次に AVPlayerItem の再生が終了したときの通知を設定する。

たとえば再生ボタンがあるとする。そのアイコンを再生・一時停止で切り替えている場合、再生が完了したら次の再生に備えて再生アイコンにするなどの処理が必要になる。またアイコン複数のコンテンツを再生するなら次の AVPlayerItem を設定するタイミングにもなる。

AVPlayerItem 設定が完了したらそれを指定して AVPlayer を生成。これはコンテンツの再生を制御するオブジェクトとなる。play/pause メソッドで再生・一時停止をおこなう。

  • 余談
    • MPMoviePlayerController と異なりなぜか stop は用意されていない
    • iTunes もそうだけど stop を提供しないのは Apple のポリシーなのだろうか?
    • 個人的には停止が欲しいのだが

最後に AVPlayerAVPlayerLayer に指定。AVPlayerLayer は表示を担当している。前述の AVPlayerView 内に生成されているので インスタンスのアクセスは AVPlayerView.layer から得たものをキャスト。これにて初期化は完了。

再生位置スライダーと時間表示

再生・一時停止ボタンは簡単に実装できたが再生位置スライダーはすこし面倒。AVPlayer 系の CMTimeUISlider.value の相互変換を処理しなければならない。スライダーが UISlider* seekBar として定義されているなら初期化は以下のようにおこなう。

/**
 * シークバーを初期化します。
 */
- (void)setupSeekBar
{
    self.seekBar.minimumValue = 0;
    self.seekBar.maximumValue = CMTimeGetSeconds( self.playerItem.duration );
    self.seekBar.value        = 0;
    [self.seekBar addTarget:self action:@selector(seekBarValueChanged:) forControlEvents:UIControlEventValueChanged];

    // 再生時間とシークバー位置を連動させるためのタイマー
    const double interval = ( 0.5f * self.seekBar.maximumValue ) / self.seekBar.bounds.size.width;
    const CMTime time     = CMTimeMakeWithSeconds( interval, NSEC_PER_SEC );
    self.playTimeObserver = [self.videoPlayer addPeriodicTimeObserverForInterval:time
                                                                           queue:NULL
                                                                      usingBlock:^( CMTime time ) { [self syncSeekBar]; }];

    self.durationLabel.text = [self timeToString:self.seekBar.maximumValue];
}

/**
 * 再生位置スライダーを同期します。
 */
- (void)syncSeekBar
{
    const double duration = CMTimeGetSeconds( [self.videoPlayer.currentItem duration] );
    const double time     = CMTimeGetSeconds([self.videoPlayer currentTime]);
    const float  value    = ( self.seekBar.maximumValue - self.seekBar.minimumValue ) * time / duration + self.seekBar.minimumValue;

    [self.seekBar setValue:value];
    self.currentTimeLabel.text = [self timeToString:self.seekBar.value];
}

/**
 * 再生時間スライダーの操作によって値が更新された時に発生します。
 *
 * @param slider スライダー。
 */
- (void)seekBarValueChanged:(UISlider *)slider
{
    [self.videoPlayer seekToTime:CMTimeMakeWithSeconds( slider.value, NSEC_PER_SEC )];
}

CMTime から秒を取得するときは CMTimeGetSeconds 関数を利用。UISlider.maximumValue に対して AVPlayerItem.duration を変換した値を指定することで 0 ~ 演奏時間の範囲でシークできるようになる。

次に再生時間の変化に応じてスライダー更新用のタイマーを起動する。処理は Apple のサンプル プログラム AVPlayerDemo を参考にした。タイマーは定期的に syncSeekBar メソッドを呼び、そのなかで更新される。

スライダー側が操作された場合は CMTimeMakeWithSeconds で value を CMTime に変換して AVPlayer 側をシークさせる。再生位置と演奏時間を表示するテキストは以下のメソッドで生成。

/**
 * 時間を文字列化します。
 *
 * @param value 時間。
 *
 * @return 文字列。
 */
- (NSString* )timeToString:(float)value
{
    const NSInteger time = value;
    return [NSString stringWithFormat:@"%d:%02d", ( int )( time / 60 ), ( int )( time % 60 )];
}

スライダー処理により再生位置と演奏時間は UISlider.valuemaximumValue に設定されているはずなので、それを指定すれば表示用テキストを得られる。

サンプル プログラム

サンプルを起動すると動画プレーヤーのデモと iOS シミュレーター用の動画登録メニューが表示される。

サンプル プログラムのメニュー画面

動画プレーヤーを選ぶとカメラロールから動画を選択する画面が表示され、その後にデモ画面へ遷移する。サンプルのプロジェクト一式を GitHub にて公開した。開発は Xcode 4.5.1、動作確認には iPhone 5.1 シミュレーター、iPod touch 第 5世代 (昨日とどいた) を使用。

ライセンスは The MIT License (MIT)。fork、改変などはご自由にどうぞ。

Copyright © 2009 - 2023 akabeko.me All Rights Reserved.