#前置き
JUCEでマルチプラットフォーム・音楽分析アプリケーションを開発し始めて、早一年半。
主に音楽学者や作曲家向けに、高度な信号処理アルゴリズムや主成分分析、SQLなどを使った大量のデータのソーティング機能を、より身近でお手軽に使えるワークプレースを提供する目的で、ソフトウェアの開発を行っています。
プロジェクトページです。
IRiMaS (Interactive Research in Music as Sound)
音楽学(Musicology)の中でも、Ethnographyという分野では、体系的な楽譜や文章による記録手法が確立されていない民族音楽を、ビデオや画像をふんだんに使用して分析・説明する、文化人類学的なアプローチをとる必要があります。この場合、単なる動画再生ソフトだけでなく、動画上にコメントや画像、図形などを設置し、さらに再生速度などを自由にコントロールする、いわゆる簡単な動画編集機能が必要になります。
そんなのフリーの動画編集でできるよ!って言っても、複数のソフトウェアを同時に起動して作業するのは嫌だ!ってことで、音楽学に必要なあらゆる機能が一つになったソフトウェアの需要は近年高まりつつあるようです。
#本題
そこで、JUCEで動画プレイヤーを実装するために、VideoComponentを使用します。
しかしながら、VideoComponentは実装されたOSによって、OS準拠の動画プレイヤーが実装されます。
例えばMacの場合はQuickTime Playerが、NSViewに貼り付けられた上で、JUCE準拠のComponentに貼り付けられます。
ここで起こる問題が、Heavy-Weight(ヘビー君)とLight-Weight(ライトちゃん) Componentの問題です。
VideoComponentに限らず、OpenGL、Web系のComponentは全てヘビー君であり、ライトちゃんであるJuceのComponentと同じレベルで使用すると、ヘビー君が常に、あらゆるライトちゃんよりも前面に表示される、という問題が起こります。
JUCEでは、OpenGLContextに限っては、OpenGLContextの背面に隠れたライトちゃんを、OpenGLエンジンでレンダリングするという力技で、前面にライトちゃんを表示できるようにしています。
この記事では以下の二つの解決策をMacOS限定で提案しています。
・VieoComponentはどうする?
・複数のヘビー君を貼り付けた時、ヘビー君同士のZ-Orderはどう変更する?
#JUCE LibraryのNSViewComponentをハックする
せっかくマルチプラットフォームで開発しているのに、結局OS別に対応せざるを得ない結論に。
もしベターな方法を知っている人がいましたら、是非ご教示ください。よろしくお願いします。
###手順1 Juce::ComponentにOpenGLContextを実装する
ヘビー君と対等に対峙できるのは、ヘビー君だけです。
ライトちゃんをヘビー君にするには、OpenGLContextを使用します。
これは非常に簡単にできます。
Juce::Componentを継承したクラスのメンバに以下を追加。
OpenGLContext openGLContext;
そして、コンストラクトに
this->openGLContext.attachTo(*this);
これでOpenGLContextに、Juce::Componentが追加されました。
かならずディコンストラクタに以下を書きます。
this->openGLContext.detach();
こうすることで、Juce::Componentもヘビー君に変わりました。
###手順2 ヘビー同士のZ-Orderを変更する
ヘビー君同士は、作られた順番に前面に表示され、後から順番を変更することはできません。なので、これだけでは実用的ではありません。
ここから、NSViewComponentハックになります。皆さん、バックアップをとって、先に進んでください。また、この記事では最低限の非常にラフな実装を紹介しています。
では、JUCE Libraryにダイブしましょう。
1・OpenGLContextのattachTo(Component& component)にアクセス
void OpenGLContext::attachTo (Component& component)
{
component.repaint();
if (getTargetComponent() != &component)
{
detach();
attachment.reset (new Attachment (*this, component));
}
}
ここから、ひたすらcomponentの終着点まで掘り進めていきます。
Attachmentクラスのコンストラクタ。
Attachment (OpenGLContext& c, Component& comp)
: ComponentMovementWatcher (&comp), context (c)
{
if (canBeAttached (comp))
attach();
}
ここのattach()にジャンプします。
void attach()
{
auto& comp = *getComponent();
auto* newCachedImage = new CachedImage (context, comp,
context.openGLPixelFormat,
context.contextToShareWith);
comp.setCachedComponentImage (newCachedImage);
start();
}
次は、CachedImageクラスです。
ちなみに、getComponent()はOpenGLContextから渡された、ライトちゃんであるComponentクラスのポインタを返しています。
CachedImage (OpenGLContext& c, Component& comp,
const OpenGLPixelFormat& pixFormat, void* contextToShare)
: ThreadPoolJob ("OpenGL Rendering"),
context (c), component (comp)
{
nativeContext.reset (new NativeContext (component, pixFormat, contextToShare,
c.useMultisampling, c.versionRequired));
if (nativeContext->createdOk())
context.nativeContext = nativeContext.get();
else
nativeContext.reset();
}
次はNativeContextクラス!
NativeContext (Component& component,
const OpenGLPixelFormat& pixFormat,
void* contextToShare,
bool shouldUseMultisampling,
OpenGLVersion version) : owner(component) // keitaro
{
NSOpenGLPixelFormatAttribute attribs[64] = { 0 };
createAttribs (attribs, version, pixFormat, shouldUseMultisampling);
NSOpenGLPixelFormat* format = [[NSOpenGLPixelFormat alloc] initWithAttributes: attribs];
static MouseForwardingNSOpenGLViewClass cls;
view = [cls.createInstance() initWithFrame: NSMakeRect (0, 0, 100.0f, 100.0f)
pixelFormat: format];
if ([view respondsToSelector: @selector (setWantsBestResolutionOpenGLSurface:)])
[view setWantsBestResolutionOpenGLSurface: YES];
[[NSNotificationCenter defaultCenter] addObserver: view
selector: @selector (_surfaceNeedsUpdate:)
name: NSViewGlobalFrameDidChangeNotification
object: view];
renderContext = [[[NSOpenGLContext alloc] initWithFormat: format
shareContext: (NSOpenGLContext*) contextToShare] autorelease];
[view setOpenGLContext: renderContext];
[format release];
viewAttachment = NSViewComponent::attachViewToComponent (component, view);
}
やっとNSViewComponentが出現しました。ダンジョン奥地で伝説のポケモンに遭遇した気分。ですが、さらに奥に進みます。
上のコードの、
viewAttachment = NSViewComponent::attachViewToComponent (component, view);
この部分です。
ReferenceCountedObject* NSViewComponent::attachViewToComponent (Component& comp, void* view)
{
return new NSViewAttachment ((NSView*) view, comp);
}
ここで、NSViewComponentからNSViewAttachment型のデータがが返されています。NSViewAttachmentにアクセス。
NSViewAttachment (NSView* v, Component& comp)
: ComponentMovementWatcher (&comp),
view (v), owner (comp),
currentPeer (nullptr)
{
[view retain];
[view setPostsFrameChangedNotifications: YES];
updateAlpha();
if (owner.isShowing())
componentPeerChanged();
attachViewWatcher (view);
}
やっとNSViewにたどり着きました。ここの、componentPeerChanged()を見てみます。
void componentPeerChanged() override
{
auto* peer = owner.getPeer();
if (currentPeer != peer)
{
currentPeer = peer;
if (peer != nullptr)
{
auto peerView = (NSView*) peer->getNativeHandle();
[peerView addSubview: view];
componentMovedOrResized (false, false);
}
else
{
removeFromParent();
}
}
[view setHidden: ! owner.isShowing()];
}
ここで受け取ったライトちゃんComonentを、NSViewに貼り付けているのがわかります。
if (peer != nullptr)
{
auto peerView = (NSView*) peer->getNativeHandle();
[peerView addSubview: view];
componentMovedOrResized (false, false);
}
特定のNSViewを前面に持ってくる為には、以下の処理が必要です。
if (peer != nullptr)
{
auto peerView = (NSView*) peer->getNativeHandle();
[view retain];
[view removeFromSuperview];
[peerView addSubview:view];
[view release];
}
上の処理は、NSViewAttachmentの中に書くのではなく、一つ前のクラスNativeContextの中に書きます。NativeContextのクラス中の適当な箇所に、以下のメンバとメソッドを追加します。
//NSViewAttachmentと同じ要領でownerを追加
//ownerはNativeContextのコンストラクタで初期化する owner(component)
Component& owner;
void bringViewToFront()
{
auto* peer = owner.getPeer();
currentPeer = peer;
//NSViewを貼り直すことでZ-Indexを更新
if (peer != nullptr)
{
auto peerView = (NSView*) peer->getNativeHandle();
[view retain];
[view removeFromSuperview];
[peerView addSubview:view];
[view release];
}
else
{
std::cout << "removeFromParent\n";
}
[view setHidden: ! owner.isShowing()];
}
あとは、このNativeContextのbringViewToFront()メソッドをOpenGLContextから呼び出せるようにするだけです。どんどん遡りながら、public メソッドを追加していきましょう。。。
void bringViewToFront()
{
nativeContext->bringViewToFront();
}
void bringViewToFront()
{
auto& comp = *getComponent();
auto* cach = static_cast<CachedImage*>(comp.getCachedComponentImage());
cach->bringViewToFront();
}
void bringViewToFront();
void OpenGLContext:: bringViewToFront()
{
if(attachment.get() != nullptr)
{
attachment->bringViewToFront();
}
}
最後に、OpenGLContextを実装したライトちゃんに、以下のメソッドを追加
void bringViewToFront() { this->openGLContext. bringViewToFront(); }
これで、ライトちゃんを前面に持って来たい時は、このメソッドを呼び出せばOKです。
これまでは、メニューバーよりもヘビー君が前面に来て、こんな訳の分からない状態になっていたのが。。。
以下の様に!!
ただし、MacOSでのみ有効は解決策です。
#パフォーマンスは?
今の所、問題は出ていません。カクることもなく、問題なく動いています。
ただ一点、Windowサイズを手動で連続的に変更すると、メニューバーのアイコンがカクカクします。これは、OpenGLのレンダリングタイミングと、Componentのサイズ変更のタイミングにずれが生じているのが原因?と考えています。
これについては、以下の様な議論がありますが、解決策は示されていません。
Resizing stuttering on OpenGL rendered UI
誰か良い方法がありましたら、教えてください。よろしくお願いします。。。