QTKit Captureを使ってみる - その10 [プログラミング - QTKit Capture]
何回か前にCore Videoってなんじゃいな、というのをちょっとだけ見た。ディスプレイのリフレッシュに同期した表示ができるフレームワークらしい。
ちょっと試しに使うことにして、ガイドにあるコードの例をほとんどそのまま引き写してやってみよう。ガイドではQuickTimeムービーを読み込んで表示している。ここではその代わりにCore Imageでやったのと同じようにsetVideoBuffer:メソッドがQTCaptureDecompressedVideoOutputから呼ばれてバッファの中身がセットされるようにしよう。 まずインターフェイスのインスタンス変数。
@interface MyVideoView : NSOpenGLView { NSRecursiveLock *lock; CVDisplayLinkRef displayLink; CVImageBufferRef currentFrame; CIContext *ciContext; BOOL needsReshape; } … @endそれからインプリメンテーション。ちょっと長いけど。
CVReturn MyDisplayLinkCallback ( CVDisplayLinkRef displayLink, const CVTimeStamp *inNow, const CVTimeStamp *inOutputTime, CVOptionFlags flagsIn, CVOptionFlags *flagsOut, void *displayLinkContext) { CVReturn error = [(MyVideoView*) displayLinkContext displayFrame:inOutputTime]; return error; } @implementation MyVideoView - (void)awakeFromNib { CVReturn error = kCVReturnSuccess; CGDirectDisplayID displayID = CGMainDisplayID();// 1 error = CVDisplayLinkCreateWithCGDisplay(displayID, &displayLink);// 2 if(error) { NSLog(@"DisplayLink created with error:¥%d", error); displayLink = NULL; return; } error = CVDisplayLinkSetOutputCallback(displayLink,// 3 MyDisplayLinkCallback, self); CVDisplayLinkStart(displayLink); needsReshape = YES; currentFrame = NULL; ciContext = nil; } - (void)setVideoBuffer:(CVImageBufferRef)buffer { [lock lock]; currentFrame = buffer; CVBufferRetain(currentFrame); [lock unlock]; } - (void)setNeedsReshape:(BOOL)yn { needsReshape = yn; } - (CVReturn)displayFrame:(const CVTimeStamp *)timeStamp { CVReturn rv = kCVReturnError; NSAutoreleasePool *pool; pool = [[NSAutoreleasePool alloc] init]; if(currentFrame) { [self drawRect:NSZeroRect]; rv = kCVReturnSuccess; } else { rv = kCVReturnError; } [pool release]; return rv; } - (void)setFrameSize:(NSSize)newSize { needsReshape = YES; [super setFrameSize:newSize]; } - (BOOL)getFrameForTime:(const CVTimeStamp*)syncTimeStamp { return (currentFrame != nil); }
MyDisplayLinkCallback()と言う関数がいわゆるコールバック関数で、Cocoaのデリゲートメソッドのように処理が必要になったとき(この場合、ディスプレイの描画サイクルごとに)呼び出される。これは昔のSequenceGrabberでも同じことをやった。引数の最後のvoidポインタはコールバックを登録するCVDisplayLinkSetOutputCallback()の最後の引数の値が渡される。この場合selfを登録しているのでコールバック関数の中で自分の型であるMyVideoViewにキャストして使っている。このへんのやり方は古いMacでよくあった。
コールバック関数の中身は単に自分のメソッドdisplayFrame:を呼んでいるだけ。コールバックは普通のCの関数なので、こうしないと自分のインスタンス変数にアクセスできない。
displayFrame:はgetFrameForTime:という自分のメソッドで描画フレームがあるかどうかを尋ねて、あればdrawRect:を呼んでいる。普通drawRect:は直接呼ばずにsetNeedsDisplay:をセットするだけにしろ、とreferenceにも書いてあるが、ここでは直接呼んでいる。すぐ描画するということか。
getFrameForTime:メソッドは、ガイドの例ではQuickTimeムービーを表示しているのでムービーのタイムスタンプを読んで描画が必要かどうか判断している。ここでは単にフレームバッファの中身があるかどうかだけで判断している。QTCaptureDecompressedVideoOutputでもらってくるデータは垂れ流し(デリゲートで受ける)なのでこっちのコールバックのタイミングとは同期しない。QTCaptureで入力画像を作る側と、Core Video側でそれを表示する側でフレームバッファへのポインタの値から
ポインタの値 | 画像入力側での解釈 | 表示側での解釈 |
---|---|---|
NULL | 表示が終わった | 画像データがまだない |
NULL以外 | 表示ができていない | 画像データがそろった |
drawRect:は
- (void)drawRect:(NSRect)theRect { [lock lock]; // 1 NSRect frame = [self frame]; NSRect bounds = [self bounds]; [[self openGLContext] makeCurrentContext];// 2 if(needsReshape)// 3 { GLfloat minX, minY, maxX, maxY; minX = NSMinX(bounds); minY = NSMinY(bounds); maxX = NSMaxX(bounds); maxY = NSMaxY(bounds); [self update]; if(NSIsEmptyRect([self visibleRect])) { // 4 glViewport(0, 0, 1, 1); } else { glViewport(0, 0, frame.size.width ,frame.size.height); } glMatrixMode(GL_MODELVIEW); glLoadIdentity(); glMatrixMode(GL_PROJECTION); glLoadIdentity(); glOrtho(minX, maxX, minY, maxY, -1.0, 1.0); CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); NSDictionary *colorDict = [NSDictionary dictionaryWithObjectsAndKeys: (id)colorSpace, kCIContextOutputColorSpace, (id)colorSpace, kCIContextWorkingColorSpace,nil]; if (ciContext) [ciContext release]; ciContext = [[CIContext contextWithCGLContext: (CGLContextObj)[[self openGLContext] CGLContextObj] pixelFormat:(CGLPixelFormatObj) [[self pixelFormat] CGLPixelFormatObj] options:colorDict] retain]; CGColorSpaceRelease(colorSpace); needsReshape = NO; } glClearColor(0.0, 0.0, 0.0, 0.0); glClear(GL_COLOR_BUFFER_BIT); [self renderCurrentFrame]; // 6 glFlush();// 7 [lock unlock];// 8 } - (void)renderCurrentFrame { CGRect frame = NSRectToCGRect([self frame]); CGRect imageRect; CIImage *inputImage; if (currentFrame != NULL) { inputImage = [CIImage imageWithCVImageBuffer:currentFrame];// 1 imageRect = [inputImage extent];// 2 [ciContext drawImage:inputImage // 3 inRect:frame fromRect:imageRect]; CVBufferRelease(currentFrame); currentFrame = NULL; } } @endOpenGLの関数が並ぶ。うーん、実はぜんぜんよくわかっていない。射影変換のマトリクスを作って表示領域いっぱいになるようにする。その後画像データを描いてフラッシュする、ということをやっているのだろう。
これでとりあえず表示はできた。スクリーンキャプチャの証拠写真。
640×480で表示してアクティビティモニタのCPUの値は25〜30%ぐらい。QTCaptureViewにせまる軽さだった。
ガイドのもとのコードではQuickTimeムービーを設定するところでOpenGLのテクスチャをコンテクストにセットしている。でもそれがなくても動く。よくわからん。
ウィンドウがリサイズされるとsetFrameSize:が呼ばれるけど、このままではクラッシュした。しかもOSを巻き添えにして。なんでなのかわからないし、ディスプレイがディムして「リセットボタンを押してください云々」を見るのは精神衛生上よろしくないので、たびたび試してみるわけにもいかない。OpenGL(と、Mac OSX独自実装のところも含めて)をまじめに勉強してからにしよう。このままではダメね。
Macの利用者の方でしたらご存じないとは思いますが、KeyHoleTVと
いうアプリケーションの作者です。Mac利用者からKeyHoleTVの移植の要請がたくさんあって、移植を試みている最中です。Linux版のKeyHoleTV(GTK利用)をMacに移植したら、映像を出す処理が異常に遅いことがわかりました。(GTKは,Macのネイティブを使っているフレームワークです。)表示形だけ、SDLというライブラリを使うと、動画はなんとか見れることができましたが、今度は、イベントがとれない問題にあたりました。ビデオ表示の方法を模索中でしたので、大変参考になりました。ありがとうございます。
by Takashi Kosaka (2008-10-10 10:06)
コメントありがとうございます。あのKeyHoleTVの作者のかたからコメントいただけるのは光栄です。私のような素人のお勉強が、専門家の方の参考になるとはうれしいです。よろしくお願いします。
by decafish (2008-10-11 13:53)
Cocoaプログラム初心者です。
一連のQTKit Captureを使ってみる - の記事を拝見して参考にしたいと思うのですが語られてない部分を補完して試すだけの力がまだないので、できればXcodeプロジェクト全体を公開していただけませんでしょうか。
コードを動かしながら追って行きたいです。
一方的なお願いで恐縮ですが、ご検討いただけますと幸いです。
by maru (2010-03-07 14:49)
了解しました。実はもうすでに中身をすっかり忘れてしまっているので、とりあえず一番簡単なQTCaptureViewを使っているものを
http://www011.upp.so-net.ne.jp/decafish/CocoaCodes/DVStreamViewer.zip
に置きました。
コメントもないし、エラー処理もいいかげんなのでお役に立てればよいのですが...
問題ありましたら、またコメントにあげていただければと思います。
よろしくお願いします。
by decafish (2010-03-07 17:09)
早速の対応ありがとうございます。
ダウンロードさせていただきビルド・実行してみました。データサイズ・表示サイズを切り替える機能などは自分で実装したことがないので参考になりそうです。
一番知りたい(わからなくて悩んでいる)のはこの記事(その10)で扱われているCore Videoを使ってOpenGL経由でレンダリングする方法についてなのですが、このエントリのコードを拝見することはできませんでしょうか。
現在複数カメラからの画像を取得して処理を施して表示する、ということを実現したいと考えておりまして、描画速度とできることの自由度を考えると(アップル社提供のドキュメントを眺めて見ての判断ですが)CoreVideoを使うことが選択肢になるのではないかと思い調査しているところです。Core Video Programming GuideではQTMovieからの入力を扱う前提で記事が書かれていて、さらに動く状態のコードが提供されていないので難儀しているところにこちらのブログを見付けました。
> setVideoBuffer:メソッドがQTCaptureDecompressedVideoOutputから呼ばれてバッファの中身がセットされるようにしよう。
このようにさらっと説明されているところから習熟した方には自明なものなんだろうと思いますので恐縮なのですが、こちらのエントリで紹介されているプログラムのXcodeプロジェクトを公開いただけますと助かります。よろしくお願いします。
# ウェブで検索してもMacOSX固有の開発関連はあまりに情報が少ないので驚いているところです。
by maru (2010-03-07 18:26)
それは失礼しました。ただ、OpenGLは僕もよく理解してなくてなぜか表示はするのですが正しいやり方かどうかわかりません。このコードを
http://www011.upp.so-net.ne.jp/decafish/CocoaCodes/DVStreamViewer5.zip
に置きます。どこまでご参考になるかわかりませんが。
QTKit Captureではメインスレッドで入力デバイスごとに複数のストリームを受けられるようになっている(よけいなスレッドは勝手にできますが)らしいのですが、例えば3Dにしようとしたらフレームの同期を取るための機能(タイムスタンプ比較とか)などは見当たりません。
なにか面白いことがわかったらまたお教えください。
よろしくお願いします。
by decafish (2010-03-07 20:32)
たびたびありがとうございます。
ビルド・動作確認するところまでできました。このコードを精査して調査を進めていきたいと思います。取っ掛かりがなくて困っていたところなのでとてもありがたいです。また何かありましたらよろしくお願いします。
by maru (2010-03-07 22:44)
ARの研究をしているイグナチオです。GLUTからMacのネイティブのプログラムを書こうとしていて CoreVideoの大変勉強になりました。私はOpenGLを使って様々なオブジェクトを表示したいと考えているので、CoreView+NSOpenGLViewの組み合わせの見本となるプログラムを探していました。
大変感謝しています。ありがとうございました。
私も無事にビルドし実行することができました。
(私の場合QTCaptureDeviceInputのcaptureAudioDeviceInputは不要で コメントにしました。そしてQTMediaTypeMuxedをQTMediaTypeVideoに変更することで実行できました。)
by イグナチオ (2010-04-09 17:10)
コメントありがとうございます。
お役に立ててうれしいです。
ただ、僕はOpenGLのほうをちゃんとわかってなくて、コードはとりあえず動きますが何かするとすぐクラッシュしてしまいますし....
お恥ずかしい限りです。
by decafish (2010-04-10 05:54)