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 など) を探して入れておく。
次に動画をカメラロールに保存する処理を実装。
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 から消しておいたほうがいい。
フルスクリーン
動画プレーヤーとなる画面はフルスクリーンにしたいので MPMoviePlayerController
、AVPlayer
それぞれのデモ画面で以下の処理を実行している。
/**
* 画面が表示される時に発生します。
*
* @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];
}
表示してみる。
再生パネル (と呼ぶのだろうか?) 右端に置かれた矢印のボタンをタップするとフルスクリーンに移行。このときステータス バーとナビゲーション バーは自動的に隠される。
再生コントロールを自前で用意したい場合は controlStyle
プロパティに MPMovieControlStyleNone
を指定して play/pause/stop
メソッドなどを制御することになる。しかしその目的なら自由度の高い AVPlayer
のほうが向く。
AVPlayer
純粋な動画再生 & 表示機能だけ欲しくて UI を自作するつもりなら AVPlayer
がよい。このクラスを利用する場合はプロジェクトに AVFoundation.framework を追加する。また AVPlayer
関連では CMTimer
などを操作をする機会が多いので CoreMedia.framework を追加してユーティリティ関数を使用できるようにしておく。
UI
AVPlayer
は操作用コントロールを一切、用意していないため自作が必要となる。とりあえず今回は以下の構成で実装することにした。
画面下部がコントロールとなる。左から
- 再生・一時停止ボタン
- 再生時間
- 再生位置スライダー
- 演奏時間
となる。
AVPlayerView
AVPlayer
は単なる動画プレーヤーなので表示には UIView
と AVPlayerLayer
が要る。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
を返す処理。これで AVPlayerView
が AVPlayerLayer
をホストするようになる。初見では UIView.layer - addSublayer
すればよいのでは?と思ったのだが、それでは正しく動作しないらしい。この実装が必須だとしたら AVPlayer
関連の処理も AVPlayerView
に局所化したほうがよいのかもしれない。
AVPlayer の初期化
AVPlayerView
を実装したので AVPlayer
を組み合わせて動画再生してみる。まず UIViewController
に AVPlayerView
が配置されているとした場合、初期化は以下のようになる。
// 以下が定義されているものとする。
// 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 のポリシーなのだろうか?
- 個人的には停止が欲しいのだが
最後に AVPlayer
を AVPlayerLayer
に指定。AVPlayerLayer
は表示を担当している。前述の AVPlayerView
内に生成されているので インスタンスのアクセスは AVPlayerView.layer
から得たものをキャスト。これにて初期化は完了。
再生位置スライダーと時間表示
再生・一時停止ボタンは簡単に実装できたが再生位置スライダーはすこし面倒。AVPlayer
系の CMTime
と UISlider.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.value
、maximumValue
に設定されているはずなので、それを指定すれば表示用テキストを得られる。
サンプル プログラム
サンプルを起動すると動画プレーヤーのデモと iOS シミュレーター用の動画登録メニューが表示される。
動画プレーヤーを選ぶとカメラロールから動画を選択する画面が表示され、その後にデモ画面へ遷移する。サンプルのプロジェクト一式を GitHub にて公開した。開発は Xcode 4.5.1、動作確認には iPhone 5.1 シミュレーター、iPod touch 第 5世代 (昨日とどいた) を使用。
ライセンスは The MIT License (MIT)。fork、改変などはご自由にどうぞ。