Cocos2dxで動画の上にスプライトを描画する(iOS)

2014-06-18

iOSでCocos2dxを使ったアプリで動画を再生したい場合のことがcocos2d-x 勉強第6回「ムービーを再生してみる」 - 株式会社BEFOOLに書いてある(そのサイトが参照しているのがCocos2d-x Cross Platform Video Player – Part 1 iOS | Gethu Games – Blog)。

やり方としては、cocos2dxのライブラリ内であるcocos2dx/platform/ios/CCEAGLView に手を加えて、UIViewを継承したCCEAGLViewに、簡単に動画を再生できるMediaPlayer.frameworkのMPMoviePlayerControlleraddSubviewしている。

その通りで再生できるんだけど、これだと動画が一番上に表示されてしまって、その上にメニューとかをCocos2dxのスプライトで出そうとしても動画に隠されてしまう。それは困るということで、動画を一番下に持って行きたい。addSubviewの代わりに、insertSubview:atIndex:0 としてみたが、動画が変なふうに再生されてしまうようになった。詳しくは、アプリをランドスケープモード専用にしているのに、動画はポートレイトの画面レイアウトのまま再生しようとする。

なんかiOSアプリの動作がよくわかってないので、どうしてそうなるのか、また解決方法もよくわからなかった。Orientationが変更された時にUIWindowRootViewControllerへのコールバックが呼ばれてUIViewCALayerとかを辿るんじゃないかと勝手に想像してあれこれやってみたが、うまくいかない…。問題はeaglViewboundsがポートレイトの縦長比率になっているのが問題だと思うんだけど、どこでorientationをハンドリングしてるのかわからなかった。

しかたがないので無理やりやってみた。まずCCEAGLViewに動画の再生を突っ込むのはやめて、cocos2dxのプロジェクトを作成した時にiOSの機種固有ソースとして生成されるAppControllerをいじる。まずヘッダにいろいろ追加する。動画の現在の再生位置とかを取得したいので、MPMoviePlayerControllerじゃなくAVFoundation.frameworkのAVPlayerに変更した:

// AppController.h
...
// インポートを追加
#import <AVFoundation/AVPlayer.h>
#import <AVFoundation/AVPlayerItem.h>
#import <AVFoundation/AVPlayerLayer.h>

...
@interface AppController : NSObject {
UIWindow *window;

// 動画再生用のメンバ変数を追加
UIView* mainUiview;
AVPlayer* player;
AVPlayerItem* playerItem;
AVPlayerLayer* playerLayer;
}

@property(nonatomic, readonly) RootViewController* viewController;

// 動画再生用のメソッドを追加
- (void) playVideo:(NSString *)path;
- (void) stopVideo;
- (void) pauseVideo: (BOOL)pause;
- (void) seekVideo: (float) position;
- (void) getVideoInfo: (float*)now duration:(float*)duration;

@end

AppControllerの実装ファイル .mmでRootViewControllerへのビューとしてCCEAGLViewを与えているところを、動画をその下で再生したいので一段かまして、UIViewを挟んでやる。でそうするとなぜかboundsorientationが反映されなくなってしまうので、フレームサイズを無理やりランドスケープ(横長)にしてやる。また動画を見せるためにCocos2dxで描画してない領域を透過させてやる必要があるので、opaque = NOをセットする:

// AppController.mm
#import <AVFoundation/AVAsset.h>

+static AppController* theController;

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
+ // iOSWrapperからAppControllerが呼び出せるように、グローバル変数に格納しておく
+ theController = self;

...

// Init the CCEAGLView
+ CGRect rect = [window bounds];
+ {
+ // 横長になるように強制的に書き換える
+ float w = rect.size.width, h = rect.size.height;
+ if (w < h) {
+ rect.size.width = h; rect.size.height = w;
+ }
+ }
- CCEAGLView *eaglView = [CCEAGLView viewWithFrame: [window bounds]
+ CCEAGLView *eaglView = [CCEAGLView viewWithFrame: rect
pixelFormat: kEAGLColorFormatRGBA8
depthFormat: GL_DEPTH24_STENCIL8_OES
preserveBackbuffer: NO
sharegroup: nil
multiSampling: NO
numberOfSamples: 0];
+ eaglView.opaque = NO;

+ // 動画をCocosよりも下に表示するために、
+ // eaglViewをrootViewController.viewに直接代入するのではなく、UIViewを挟む。
+ UIView* uiview = [[UIView alloc] initWithFrame: [window bounds]];
+ [uiview addSubview:eaglView];
+ mainUiview = uiview;

// Use RootViewController manage CCEAGLView
_viewController = [[RootViewController alloc] initWithNibName:nil bundle:nil];
_viewController.wantsFullScreenLayout = YES;
- _viewController.view = eaglView;
+ _viewController.view = uiview;

動画再生関連のメソッド:

// AppController.mm
- (void) stopVideo {
[self killMoviePlayer];
}

- (void) pauseVideo: (BOOL)pause {
if (pause)
[player pause];
else
[player play];
}

- (void) playVideo:(NSString *)path {
NSURL* url = [NSURL fileURLWithPath:path];
playerItem = [[AVPlayerItem alloc] initWithURL:url];

player = [[AVPlayer alloc] initWithPlayerItem:playerItem];

CALayer* parentLayer = self->mainUiview.layer;
playerLayer = [AVPlayerLayer playerLayerWithPlayer:player];
playerLayer.frame = parentLayer.bounds; // frame は Orientation を反映してない、bounds はしている
[parentLayer insertSublayer:playerLayer atIndex:0];
[playerLayer addObserver:self
forKeyPath:@"readyForDisplay"
options:NSKeyValueObservingOptionNew
context:NULL];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if ([keyPath isEqualToString:@"readyForDisplay"]) {
[playerLayer removeObserver:self
forKeyPath:@"readyForDisplay"
context:NULL];

[player play];
}
}

-(void)killMoviePlayer {
if (player == nil)
return;

[player pause];
[[NSNotificationCenter defaultCenter] removeObserver:self
name:AVPlayerItemDidPlayToEndTimeNotification
object:playerItem];

[playerLayer removeFromSuperlayer];

player = nil;
playerItem = nil;
playerLayer = nil;
}

- (void) seekVideo: (float) position {
if (player == nil)
return;

CMTime time = playerItem.asset.duration;
time.value *= position;
[player seekToTime: time];
}

- (void) getVideoInfo: (float*)now duration:(float*)duration {
if (player == nil) {
*now = *duration = 0;
return;
}

*now = CMTimeGetSeconds(player.currentTime);
*duration = CMTimeGetSeconds(playerItem.asset.duration);
}

Cocos2dxで画面をクリアする設定はCCDirector::setGLDefaultValues()内で

glClearColor(0.0f, 0.0f, 0.0f, 1.0f);

とダイレクトに書かれていて、アルファが1.0なのでこのままでは下に描かれている動画を上書きしてしまう。直接ここを書き換えてもいいんだけど一応ライブラリ内なので、アプリのソースClassees/AppDelegate::applicationDidFinishLaunching()内で、glview->setDesignResolutionSize()した後に glClearColor(0,0,0,0);としてやる。

でiOSWrapperからAppController内の動画関連のメソッドを呼び出すようにしてやり、リンクする追加ライブラリとCoreMedia.frameworkを追加してやることで、動画の上にスプライトで再生・一時停止ボタンや現在再生中の位置などを表示できるようになった。

動画の下にもなにか背景を描画したいとかなった場合にはどうしたらいいのかしらん。

追記

英語のページでのAndroid版のほうは、Intentを飛ばして別のアクティビティを起動して、動画を再生していた: http://www.gethugames.in/blog/2013/09/cocos2d-x-cross-platform-video-player-part-2-android.html それだったらiOSでも動画再生は別のViewControllerに切り替えてしまってもいいんじゃないの?