Apple Developer Connection
高度な検索
Member Login ログイン | ご入会 ADC連絡先

Technical Note TN2125
QuickTimeにおけるスレッドセーフなプログラミング

1992年にQuickTime 1.0を紹介したテクニカルノートは、「世界は静止していません。急速に動いています」というフレーズで始まりました。この12年、QuickTimeは変化に対応して、ほんの500のAPIから2500以上に進化し、新しいビデオ、静止画、およびオーディオフォーマット、ストリーミング、インタラクティブ機能などのサポートが追加されています。Mac OS X 10.3のリリースに伴い、QuickTimeのこのような進化は継続され、デベロッパは、プロセッサに集中する一般的なQuickTimeの処理を、よりパワフルでレスポンスのよいアプリケーションを可能にするバックグラウンドスレッドに移行できます。

このテクニカルノートは、Mac OS X 10.3以降をターゲットにしていて、アプリケーションやコンポーネントにマルチスレッドのサポートを追加したいデベロッパを対象にしています。

QuickTimeのマルチスレッドのサポートを追加することに興味があり、マルチスレッドアプリケーション開発の用語や概念をよく知らないという方には、以下の参考資料が役立ちます。

技術書一覧

Mac OS X 10.3にはQuickTime 6.4が付属しています。QuickTimeのマルチスレッドサポートを利用するには、Mac OS X 10.3 以降で作業する必要があります。それ以前の Mac OS X には、たとえ最新版のQuickTimeがインストールされていても、バックグラウンドスレッドからQuickTimeを呼び出すのに必要なシステムレベルサポートが用意されていません。





はじめに

従来のMac OS(System 6.0.8~Mac OS 9)においてQuickTimeが残したものが意味していることは、オリジナルAPIの多くがシングルスレッドから呼び出すように設計されていたということです。しかし、Mac OS XはMacintoshデベロッパに真のマルチプロセス処理のパワー、すなわち、一度に複数のアプリケーションで複数の処理を実行する能力をもたらしました。Mac OS Xはさらに、単一のアプリケーションの中から複数のスレッドで実行できるタスクの種類に関する従来のMac OSの制限を取り除きました。

QuickTimeアプリケーションを開発する場合、なぜこれが重要なのでしょうか?

この疑問に答えるために、あらゆる演算処理が一瞬で終わると仮定しましょう。これが真ならば、理論上は、すべての処理を同じスレッドから実行できることになります。しかし現実には、多くの一般的な処理は実際に時間がかかります。このようなプロセッサを集中的に使用する処理が、ユーザインターフェイスをブロックしたり、同時に実行できるタスクの数を制限したりすることにより、アプリケーションの応答を遅らせていると、ユーザエクスペリエンスが損害を受けることになります。ユーザとの直接的な対話を必要としないQuickTimeの処理を実行でき、メインスレッド上で行われていることに関係なく実行できれば、アプリケーションにとって有利です。

たとえば、ムービー圧縮について考えます。これは多少時間のかかる処理です。おそらく、アプリケーションで2つのムービーを同時に圧縮したり、1つのムービーを再生しながら別のムービーを圧縮したくなることがあります。Mac OS X 10.3以前は、このようなタイプのQuickTime処理は、容認できないほど処理を遅らせたり、アプリケーションが応答しなくなる場合がありました。

さて、この処理をメインスレッド(ユーザインターフェイスをブロックするスレッド)からバックグラウンドスレッド、または複数のバックグラウンドスレッドに移行することを考えてください。アプリケーションは、さまざまなフォーマットの複数のムービーを同時に圧縮してエクスポートしながら、ユーザインターフェイスをブロックせずに別のムービーを再生したり、他のタスクをすべて実行することができるようになります。

Mac OS X 10.3およびそれ以降におけるQuickTimeマルチスレッドサポートの設計目標は、特にこのような問題を対象にしています。現在、QuickTimeは、プロセッサを集中的に使用するタスクをバックグラウンドスレッドに移行してメインスレッドを解放し、それによってアプリケーションの速度、応答、および全体的なパフォーマンスを改善できるようになっています。

先頭に戻る

スレッドセーフ処理

Mac OS X 10.3以降では、QuickTimeの次の処理をバックグラウンドスレッドから実行できるようになりました。

  • ムービーファイルを開く

  • ムービーのレンダリング

  • ムービーの圧縮とエクスポート

  • 静止画のインポートとエクスポート

このようなタイプの処理は、処理に非常に時間がかかり、CPUのボトルネックやユーザインターフェイスの応答の問題につながる可能性があります。このような処理はバックグラウンドスレッドに移行するのが理想的です。

注:ムービーの再生は常にユーザインターフェイスのブロックを回避してきました。この方法はQuickTime 1.0以来、Mac OS X 10.3でも継続しています。

スレッドセーフにできるものについて述べる前に、QuickTimeムービー、ムービーファイル、QuickTimeオブジェクト、相互非関連(Disjoint)など、いくつかの用語を定義する必要があります。

  • QuickTimeムービー - 時間ベースのメディアを参照するメモリ内の不透過なQuickTimeデータ構造。

  • ムービーファイル - ディスク上にあり、いくつかのメディアサンプルデータを含むファイル。ムービーファイルはQuickTimeムービーファイル(.mov)であっても、そうでなくても構いません。たとえば、ムービーファイルがインポートしたMPEG4ファイル(.mp4)であっても、QuickTimeムービーとして表示することができます。

  • QuickTimeオブジェクト - このテクニカルノートでは、この用語は任意のQuickTimeデータ型を示す総称として使用します。QuickTimeオブジェクトは、QuickTimeムービーのような不透過なデータ構造、Component Instance、Decompression Sequenceなどのことです。

  • 相互非関連 - 共通の要素を持っていないこと。

注: QuickTimeムービー(メモリ内のデータ構造)とムービーファイル(ディスク上のファイル)の間で、上記の違いを理解することは重要です。

スレッドセーフにできるもの

以下のツールボックスマネージャと連携するQuickTimeアプリケーションはスレッドセーフにできます。

  • Movie Toolbox (Import, Rendering, Creation, Export)

  • Image Compression Manager

  • QuickDraw (Offscreen GWorlds)

  • Component Manager

  • Alias Manager

  • Memory Manager

アプリケーション中の各スレッドは、複数のスレッドからの論理的に相互非関連であるQuickTimeオブジェクトのセットを安全に使用することができます。あるスレッドが独自のQuickTimeオブジェクトのセットを持っており、別のスレッドが独自のセットを持っている場合、これらのオブジェクトに対して実行される処理はスレッドセーフと見なすことができます。2つのスレッドに共通のQuickTimeオブジェクトがあってはなりません。図1を参照してください。

たとえば、2つのバックグラウンドスレッドは .DVストリームをインポートし、GetNextInterestingTimeSetMovieTimeなどのAPIを使用して各ムービーを1ステップずつ進めて、MoviesTaskを呼び出してフレームをレンダリングできます。

図1:相互非関連アクセスはスレッドセーフ

図1 相互非関連アクセスはスレッドセーフ

これに対して、2つのスレッドが同じQuickTimeオブジェクトに同時にアクセスする場合、それらのオブジェクトに対して実行される処理はスレッドセーフと見なされません。これらのオブジェクトを操作するスレッドは、いずれかのオブジェクトアクセスを中心にして独自の同期(またはロック)を実行する必要があります。これは、複数のスレッドからアクセスする必要がある保護されていない(すなわち、グローバルな)データ構造と違いはありません。ある種のスレッド保護は、同時アクセスを許可する前に実施する必要があります。図2を参照してください。

図2:同時アクセスはスレッドセーフではない

図2 同時アクセスはスレッドセーフではない

注:POSIXのpthread APIは2つのスレッド同期プリミティブ、つまりミューテックス(mutex)と条件変数を提供しています。

ミューテックスは単純なロックで、2つのバックグラウンドスレッド間で共有するGraphics Importer InstanceまたはDecompression SequenceのようなQuickTimeオブジェクトへのアクセスを制御するのに使用できます。

ミューテックスには、ロックとアンロックの2つの状態しかありません。POSIXの条件変数はミューテックスと一緒に使用し、スレッドをブロックして別のスレッドからの信号を待たせることができます。信号が届いたら、ブロックされていたスレッドは関連するミューテックスでロックをグラブしようと試みることができます。

マルチプロセッシングサービスも、スレッド同期とシグナリングメカニズムを提供します。

pthreadとマルチプロセッシングサービスに関する詳細は、このテクニカルノートの最後にある「参考資料」のセクションを参照してください。

ファイルシステムを扱う場合は、例外が発生します。ファイルへのアクセスを実装するオペレーティングシステムルーチンは、すべてスレッドセーフです。ファイルシステムがマルチスレッドアクセスに合わせて設計されているため、独立したスレッドで2つのQuickTimeオブジェクトに同じファイルを参照させることができます。2つのスレッドに読み取りのために同じムービーファイルを共有するのはまったく問題ありません。2つのスレッドでは、それぞれのQuickTimeムービーが同じムービーファイルを参照し、読み取り処理を実行することができます。たとえば、これらのスレッドでは何の問題なく、ムービーの異なる部分を同時にレンダリングまたは表示することができます。しかし、一方のスレッドでムービーファイルに書き込みながら、他方のスレッドでロックしないで読み取るのは安全でない可能性があります。図3を参照してください。

図3:読み取りのために同じムービーファイルを共有するのはスレッドセーフになります

図3 読み取りのために同じムービーファイルを共有するのはスレッドセーフになります

この議論の要点は、スレッドセーフであることを保証したい場合は、アプリケーションで複数のスレッドが同じQuickTimeオブジェクトを同時に操作しないようにするということです。このことを確実にするのは呼び出し側の責任です。

先頭に戻る

スレッドセーフでないもの

以下のツールボックスマネージャはスレッドセーフでないため、QuickTimeアプリケーションではそれらのサービスをバックグラウンドスレッドから使用できません。

  • HIToolbox(ユーザインターフェイス)

  • Resource Manager

さらに、以下のMovie Toolbox機能もバックグラウンドスレッドから使用できません。

  • Movie Playback

  • Movie Controller

Human Interface Toolboxは、アプリケーションにユーザインターフェイス要素を提供します。HIToolboxを使った作業はスレッドセーフではありません。さらに、ユーザが生成するすべてのイベントを通常受け取るのは、メインスレッドの実行ループです。

CarbonとCocoaのどちらの環境でQuickTimeを扱うかに関係なく、ユーザインターフェイス要素はメインスレッド上になければならず、バックグラウンドスレッド上でダイアログやウインドウを作成または表示しないようにするのは呼び出し側の責任です。QuickTimeユーザインターフェイス要素には、Standard Compression Dialog、MovieExportDoUserDialogなどのAPIによって表示されるダイアログ、デフォルトのMovie Export Progress Procedureなどがあります。図4を参照してください。

図4:すべてのユーザインターフェイスをメインスレッドに保持

図4 すべてのユーザインターフェイスをメインスレッドに保持

Resource Managerの使用も、そのAPIにリソースチェーンというグローバル状態の使用が伴うため、スレッドセーフではありません。リソースチェーンは、開いているリソースファイルのリストです。これは事実上、大部分のResource Manager APIに渡される間接的なパラメータです。つまり、バックグラウンドスレッドから、OpenComponentResFileOpenAComponentResFileCloseComponentResFileなどのAPIを呼び出すべきではないということです。

Movie Toolboxは時間ベースのデータをインポート、再生、レンダリング、作成、編集、保存できる機能を提供します。ムービーはバックグラウンドスレッドでは再生できないため、StartMovieSetMovieRateなど、Movie Toolboxの再生用APIの呼び出しは、メインスレッドからのみ実行する必要があります。

Movie Controller(よく知られているHIToolbox Controlに関して考える場合はCarbon Movie Control)は、QuickTimeムービーを処理する際に完全な機能スイート(再生、編集、ユーザインターフェイス、対話型イベントディスパッチなど)を提供します。しかし、Movie Controllerはスレッドセーフではなく、メインスレッドからのみ作成・使用する必要があります。

先頭に戻る

アプリケーションデベロッパへのアドバイス

ムービーファイルを開いたり、画像のフォーマットを変換したりするいくつかの高レベル処理の実行には、Movie Importers、Data Handlers、Image Decompressorsなど、いくつかの低レベルコンポーネントが必要になることがあります。これらのコンポーネントは、スレッドセーフかどうかが分からず、多くの場合、アプリケーションは処理が開始するまで、どの低レベルコンポーネントが呼び出されるかを知る方法がありません。

つまり、バックグラウンドスレッドからは、いくつかのメディアファイルを開くことができず、一定のメディア変換を安全に実行することができないということです。アプリケーションでは、このような可能性に対処するための計画を取り入れる必要があります。

Mac OS X 10.3には、スレッドセーフおよび非スレッドセーフコンポーネントがいくつか含まれています。たとえば、JPEG、PNG、およびTIFFなど、最も広く利用されている静止画フォーマットはスレッドセーフですが、MacPaintはスレッドセーフではありません。この例に示したケースは、スレッドセーフでないコンポーネントを必要とするメディアを自分のコードがどれだけきちんと扱えるかをテストするための厳密に定義されたケースとして利用できます。「ほとんどの場合、QuickTimeオブジェクトをメインスレッドに移行して、処理を継続する必要があります。このテクニックについては、この記事の「シナリオとサンプルコード」のセクションにあるスレッドインポートとエクスポートのサンプルで示してあります。

EnterMoviesOnThreadの呼び出し

バックグラウンドスレッドでQuickTimeを使用するアプリケーションは、バックグラウンドスレッドで他のQuickTime APIを呼び出す前に、各バックグラウンドスレッドでEnterMoviesOnThreadを呼び出す必要があります。QuickTimeがバックグラウンドスレッドで使用されなくなると、そのスレッドがExitMoviesOnThreadを呼び出します。これはQuickTimeに、アプリケーションがそのスレッドからQuickTimeを使用しないことを示します。

EnterMoviesOnThread / ExitMoviesOnThreadのペアは、EnterMovies / ExitMoviesのペアと同じではありません。QuickTime APIをメインスレッドで使用する場合、アプリケーションで引き続きEnterMoviesを呼び出す必要があります。

EnterMoviesOnThreadはスレッド固有のQuickTime環境を初期化するので、たとえば、GetMoviesErrorまたはMoviesTask(NULL, 0)の呼び出しは、他のスレッドのエラーを取得したり、ムービーをタスク化したりしません。

EnterMoviesOnThreadの呼び出しはまた、当該スレッド上で非スレッドセーフコンポーネントを使用できないこともComponent Managerに通知します。Component Managerが特定の機能を実行するために非スレッドセーフコンポーネントを開こうとすると、componentNotThreadSafeErr (-2098)エラーが返され、そのコンポーネントは開かれません。このエラーコードは呼び出し側に伝えられます。

アプリケーションは、バックグラウンドスレッドから呼び出されたQuickTime APIからcomponentNotThreadSafeErrを受け取ることができ、バックグラウンドスレッドで実行する作業をメインスレッドにシフトする必要があるという通知としてこれを使用します。

リスト1:EnterMoviesOnThread

OSErr EnterMoviesOnThread(UInt32 inFlags)

EnterMoviesOnThreadは、アプリケーションが現在のスレッド上でQuickTime APIを使用することを 
QuickTimeに示すために使用します。

inFlags - 既存のスレッドがどのようにQuickTimeを使用するかを指示するフラグ。デフォルトの
オプションとして0を渡します。


フラグ:

kQTEnterMoviesFlagDontSetComponentsThreadMode = 1L << 0

ディスカッション:

アプリケーションは、作成したスレッドでEnterMoviesOnThreadを呼び出します。QuickTime APIを
呼び出す際に、作成したプリエンプティブスレッド上でEnterMoviesOnThreadを呼び出さないと、
グローバルなQuickTime状態がメインスレッドと共有されます。

デフォルトでは、EnterMoviesOnThreadは、QuickTimeのスレッド共有ポリシーに応じて、現在の
Component Managerスレッドモードを設定します。QuickTimeの状態がスレッドに対して非公開の場合は、
モードがkCSAcceptThreadSafeComponentsOnlyMode に設定されます。QuickTimeの状態が
EnterMoviesOnThreadを呼び出すスレッドとメインスレッド間で共有されている場合は、モードが
kCSAcceptAllComponentsModeに設定されます。

kQTEnterMoviesFlagDontSetComponentThreadModeフラグをEnterMoviesOnThreadの呼び出しに
インクルードすると、スレッドモードは変更されず、呼び出し前の状態のままです。スレッドモードの
設定は、EnterMoviesOnThreadによって便宜を図るために提供されており、CSSetComponentsThreadModeを
使用して直接行うことができます。また、CSGetComponentsThreadModeを呼び出して、現在の
スレッドモードを取得することもできます。

EnterMoviesOnThreadまたはCSSetComponentsThreadModeを呼び出して設定したスレッドモードは 
プロセスグローバルなものではなく、スレッドローカルなものです。

EnterMoviesOnThreadの最初の呼び出しは、kQTEnterMoviesFlagDontSetComponentsThreadModeフラグを 
渡さない限り、Component Managerのスレッドモードを変更します。それに続く呼び出しはすべて、
それに続く呼び出しはすべて、Component Managerのスレッドモードをそのままにします。

EnterMoviesOnThreadの複数の呼び出しは、単一スレッド上で行うことができます。スレッドを作成し、
EnterMoviesOnThreadを呼び出すアプリケーションはその一例です。その場合、アプリケーションは
QuickTimeを使用するライブラリコードも呼び出します。ライブラリコードは、呼び出し側が
QuickTimeを初期化したかどうかを予測できないため、EnterMoviesOnThreadも呼び出します。
これは、ライブラリによる現在一般的なEnterMoviesの用法に合っています。

リスト2:ExitMoviesOnThread

OSErr ExitMoviesOnThread(void)

ExitMoviesOnThreadは、アプリケーションが現在のスレッド上でQuickTime APIを使用しないことを
QuickTimeに示すために使用します。

ディスカッション:

ExitMoviesOnThreadは、処理を完了できなかった場合に、適切なオペレーティングシステムまたは
QuickTimeエラーを返します。このエラーは、事前にEnterMoviesOnThread呼び出しをしなかった
ために発生する可能性があります。

ExitMoviesOnThreadは、QuickTimeを使用して、EnterMoviesOnThreadで実行した設定を取り消す
バックグラウンドスレッドを終了する前に呼び出してください。

それぞれのEnterMoviesOnThread呼び出しは、ExitMoviesOnThreadと対応させる必要があります。
対応するEnterMoviesOnThreadを事前に呼び出さずに、ExitMoviesOnThreadを呼び出すことはできません。

バックグラウンドスレッドで最後のExitMoviesOnThreadを呼び出した後、最初に
EnterMoviesOnThreadを呼び出さないでQuickTime APIを呼び出すと、アプリケーションはあたかも
EnterMoviesOnThreadとExitMoviesOnThreadのペアを使用しなかったかのように、メインスレッドの 
状態を共有するスレッドになります。これは互換性のためです。

ExitMoviesOnThreadを呼び出さないことは、致命的ではありませんが、場合によってはリソースリークに
つながることがあります。そのため、呼び出し側は、最初のEnterMoviesOnThreadと最後の
ExitMoviesOnThreadの間にあるすべてのQuickTime呼び出しを、二次スレッドで囲む必要があります。

先頭に戻る

スレッド間でのQuickTimeオブジェクトの移動

場合によっては、QuickTimeオブジェクトをスレッド間で移動する必要があります。

Graphics ImportersおよびGraphics Exportersのインスタンスを処理する場合は、独自のロックメカニズムを実装して、QuickTimeオブジェクト(この場合はコンポーネントインスタンス)を一度に1つのスレッドからのみ呼び出すようにすることで、これを管理する必要があります。

他方で、QuickTimeムービーは、いつでもそれらが属しているスレッドを認識する必要があります。QuickTimeムービーをスレッド間で移動する際に呼び出す必要がある2つのAPIがあります。まず古いスレッドからムービーをデタッチし、新しいスレッドにアタッチします。

QuickTimeムービーをスレッド間で受け渡すときには、古いスレッドでDetachMovieFromCurrentThreadを、新しいスレッドでAttachMovieToCurrentThreadを呼び出します。これにより、QuickTimeはムービーを所有しているスレッドを認識でき、ムービーを不正なスレッドで間違ってタスク化しないことを保証できます。図5を参照してください。

図5:スレッド間でのQuickTimeオブジェクトの移動

図5 スレッド間でのQuickTimeオブジェクトの移動

NewMovieNewMovieFromDataRefNewMovieFromFileなどのNewMovie APIを使用してムービー参照を取得すると、現在のスレッドにすでにアタッチされているムービーが作成されます。ムービーがすでにスレッドにアタッチされている場合は、AttachMovieToCurrentThreadの呼び出しが失敗します。バックグラウンドスレッドに受け渡すつもりでムービーを開いたり作成する場合は、作成スレッドでDetachMovieFromCurrentThreadを呼び出してから、バックグラウンドスレッドでAttachMovieToCurrentThreadを呼び出します。

QuickTimeムービーを含んでいる安全でないメディア(非スレッドセーフコンポーネントのレンダリングを必要とするメディア)をメインスレッドからデタッチするDetachMovieFromCurrentThreadの呼び出しが失敗し、componentNotThreadSafeErrを返します。前述したように、アプリケーションはこのような状況に対処して、メインスレッドで処理を実行したり、バックグラウンドスレッドでエラーを受け取った場合はメインスレッドに処理を移動したり、必要に応じて処理を取り消したりすることができなければなりません。

リスト3:AttachMovieToCurrentThread

OSErr AttachMovieToCurrentThread(Movie m)

m - この処理の対象となるムービー。アプリケーションは、NewMovie、NewMovieFromFile、
およびNewMovieFromHandleなどの関数を使ってこのムービーの識別子を取得します。

エラーがない場合はnoErrを、ムービーを現在のスレッドにアタッチできない場合は
componentNotThreadSafeErrを返します。

ディスカッション:

ムービーを現在のスレッドにアタッチします。

リスト4:DetachMovieFromCurrentThread

OSErr DetachMovieFromCurrentThread(Movie m)

m - この処理の対象となるムービー。アプリケーションは、NewMovie、NewMovieFromFile、
およびNewMovieFromHandleなどの関数を使ってこのムービーの識別子を取得します。

エラーがない場合はnoErrを返します。

ディスカッション:

ムービーを現在のスレッドからデタッチします。

先頭に戻る

コールバックの処理

多くのQuickTime APIによりコールバックルーチンのインストールが可能で、最も一般的なのは非同期完了コールバックとプログレスコールバックです。

非同期完了コールバックは必ず特別なスレッドから呼び出され、スレッドセーフでなければなりません。これらのスレッドは、処理を実行するスレッドと同じではありません。この動作はMac OS X 10.3でも変更されていません。

しかし、それ以外QuickTimeコールバックは、処理を実行する同じスレッドから呼び出されます。

これが特に重要なのは、いくつかのユーザインターフェイス要素を含む独自のプログレスコールバックを実装する場合です。バックグラウンドスレッドから呼び出されるプログレスコールバックは、そのスレッドで実行しています。このコードがアプリケーションのどこにあるか、または他のどの機能がそれを使用するかに関係なく、コールバックをバックグラウンドスレッドから呼び出す場合はスレッドセーフでなければなりません。

そのような場合には、プログレスコールバックを見直して、スレッドセーフにする必要があります。大変役に立つテクニックは、バックグラウンドスレッドのコールバックからメインスレッドのcarbonイベントへ送信されるカスタムcarbonイベントを使用することです。これは、バックグラウンドスレッドからスレッドセーフAPIのPostEventToQueueを使用して達成することができます。図6を参照してください。

API GetMainEventQueueは、メインのイベントキューEventQueueRef(Carbonイベントが送られるキュー)を取り出すのに使用されるAPIです。

注:Mac OS X 10.3.xでは、GetMainEventQueueはスレッドセーフではなく、メインスレッドから呼び出す必要があります。アプリケーションで、このAPIを初期に呼び出して、返されたEventQueueRefを保存し、その後でバックグラウンドスレッドに渡すスレッドデータの一部にすることができます。

Mac OS X 10.4以降ではGetMainEventQueueはスレッドセーフです。イベントを送信するバックグラウンドスレッドから呼び出せます。

図6:PostEventToQueueを使用してカスタムのCarbonイベントをメインスレッドに送信

図6 PostEventToQueueを使用してカスタムのCarbonイベントをメインスレッドに送信

重要:Movie Toolboxのデフォルトのプログレス関数の代わりにMovieProgressUPPを-1に設定して、バックグラウンドスレッドでSetMovieProgressProcを呼び出さないでください。これはスレッドセーフではありません。

ムービープログレス関数を使いたくない場合は、NULL; SetMovieProgressProc(theMovie, NULL, NULL)を使用します。

先頭に戻る

Resource Managerの処理

Resource Managerはスレッドセーフではありません。Resource Managerにはリソース、リソースチェーン、またはリソースマップを操作するAPIが含まれているということを覚えておくことが非常に重要です。

しかし、コンポーネントリソースを返す1回限りのComponent Manager呼び出しはスレッドセーフであるため、アプリケーションがコンポーネントからパブリックリソースを取得する必要がある場合は使用してください。

これらには以下のAPIが含まれています。

  • GetComponentResource

  • GetComponentIndString

  • GetComponentPublicResource

  • GetComponentPublicIndString

  • GetComponentPublicResourceList

リスト5:GetComponentPublicResourceの使用

ComponentDescription cd;
ResourceHandle resource = NULL;
Component c = 0;
   
cd.componentType = MovieExportType;
cd.componentSubType = kAComponentSubType;
cd.componentManufacturer = kAManufacturer;
cd.componentFlags = 0;
cd.componentFlagsMask = 0;
   
c = FindNextComponent(c, &cd)
if (c) {
    err = GetComponentPublicResource(c, 'PICT', 1, &resource);
    if (noErr == err) {
        // リソースへの何らかの処理
        
        ...
        DisposeHandle(resource);
    }
}

これらのAPIに関する詳細は、「流氷通信 21」 を参照してください。

先頭に戻る

シナリオとサンプルコード

(a) 静止画のインポート - ThreadsImporterサンプルコード

ThreadImporterでは、静止画を別々のスレッドにインポートして表示する方法を示しています。

(b) 静止画のエクスポート - ThreadsExporterサンプルコード

ThreadsExporterでは、異なるフォーマットの静止画を別々のスレッドにインポートしてエクスポートする方法を示しています。

(c) QuickTimeムービーのインポート - ThreadsImportMovieサンプルコード

ThreadsImportMovieでは、QuickTimeムービーを別々のスレッドにインポートして表示する方法を示しています。

(d) QuickTimeムービーのエクスポート - ThreadsExportMovieサンプルコード

ThreadsExportMovieでは、QuickTime Movie Export Componentを使用して、ムービーを別々のスレッドにエクスポートする方法をデモしています。

(e) ムービーをメインスレッドで開き、ムービーのコピーをバックグラウンドスレッドに移動します。

リスト8には、何かの処理をするために、ムービーをメインスレッドからバックグラウンドスレッドに移動する方法をデモする一般的なコードの断片が含まれています。これは、リスト6および7に示すworker関数と一緒に使用します。関数Do_SomeWorkOnSeparateThreadはQuickTimeムービーを含むWindowRefで呼び出され、これらのリストに示す関数DoSomeWorkFunctionを実行するバックグラウンドスレッドを作成します。

(f) 移動したムービーをバックグラウンドスレッドからエクスポートします。

リスト6では、QuickTime Movie Export Componentを開き、DV Codecを使用してビデオを圧縮するように設定し、次にConvertMovieToDataRefを呼び出して、バックグラウンドスレッドからのエクスポートを実行します。

リスト6:バックグラウンドスレッドのムービーをエクスポート

// このworker関数はバックグラウンドスレッドでムービーのエクスポートを実行する
void *DoSomeWorkFunction(void *inWorkerThreadRef)
{
    QTAtomContainer exportSettings;
    Handle theDataRef;
    OSType theDataRefType;
    EventRef theEventRef = NULL;
    OSStatus status;

    WorkerThreadRef worker = (WorkerThreadRef)inWorkerThreadRef;

    EnterMoviesOnThread(0);

    status = AttachMovieToCurrentThread(worker->theMovie);
    require_noerr(status, CantAttachToCurrentThread);

    CFStringRef path = CFSTR("/Users/fasteddie/Desktop/testoutput.mov");

    status = QTNewDataReferenceFromFullPathCFString(path, kQTNativeDefaultPathStyle,
                                                    0, &theDataRef, &theDataRefType);
    require_noerr(status, Done);

    // QuickTime Movie Exportコンポーネントを開いて設定する
    ComponentInstance ci = OpenDefaultComponent(MovieExportType, kQTFileTypeMovie);
    if (ci) {

        SCSpatialSettings ss;
        SCTemporalSettings ts;
        UInt8 falseSetting = false;
        QTAtom videAtom = 0;
        QTAtom sptlAtom = 0;
        QTAtom tprlAtom = 0;
        QTAtom ensoAtom = 0;
        QTAtom saveAtom = 0;
        QTAtom fastAtom = 0;

        ss.codecType = kDVCNTSCCodecType;
        ss.codec = NULL;
        ss.depth = 0;
        ss.spatialQuality = codecHighQuality;

        ts.temporalQuality = 0;
        ts.frameRate = FixRatio(30, 1); //30L<<16;
        ts.keyFrameRate = 0;

        // デフォルト値を取得して変更する - 必要に応じてこれらを保持する
        status = MovieExportGetSettingsAsAtomContainer(ci, &exportSettings);
        require_noerr(status, Done);

        // ビデオオプション
        videAtom = QTFindChildByID(exportSettings, kParentAtomIsContainer,
                                   kQTSettingsVideo, 1, NULL);
        if (videAtom) {
            // 空間オプション
            sptlAtom = QTFindChildByID(exportSettings, videAtom, scSpatialSettingsType,
                                       1, NULL);
            if (sptlAtom) {
                status = QTSetAtomData(exportSettings, sptlAtom,
                                       sizeof(SCSpatialSettings), &ss);
            }
            // 一時オプション
            tprlAtom = QTFindChildByID(exportSettings, videAtom, scTemporalSettingsType,
                                       1, NULL);
            if (tprlAtom) {
                status = QTSetAtomData(exportSettings, tprlAtom,
                                       sizeof(SCTemporalSettings), &ts);
            }
        }

        // ビデオのみ注意する
        // サウンドのエクスポートを使用不可にする。
        ensoAtom = QTFindChildByID(exportSettings, kParentAtomIsContainer,
                                   kQTSettingsMovieExportEnableSound, 1, NULL);
        if (ensoAtom) {
            status = QTSetAtomData(exportSettings, ensoAtom, sizeof(falseSetting),
                                   &falseSetting);
        }

        // インターネット向け、つまりファストスタート用に保存をオフにする
        saveAtom = QTFindChildByID(exportSettings, kParentAtomIsContainer,
                                   kQTSettingsMovieExportSaveOptions, 1, NULL);
        if (saveAtom) {
            fastAtom = QTFindChildByID(exportSettings, saveAtom,
                                       kQTSettingsMovieExportSaveForInternet, 1, NULL);
            if (fastAtom) {
                status = QTSetAtomData(exportSettings, fastAtom, sizeof(falseSetting),
                                       &falseSetting);
            }
        }

        // これらを設定する
        status = MovieExportSetSettingsFromAtomContainer(ci, exportSettings);
        require_noerr(status, Done);

        // プログレスプロシージャなし - カスタムのプログレスプロシージャを使用する場合は、
        // このスレッドで呼び出される。また、ユーザインターフェイスは使用できない。
        // ユーザインターフェイスが必要な場合に使用できる方法は、
        // カスタムのcarbonイベントを作成し、メインスレッドにインストールしたハンドラに送ること。
        SetMovieProgressProc(worker->theMovie, NULL, NULL);

        // ムービーをエクスポートする
        ConvertMovieToDataRef(worker->theMovie, 0, theDataRef, theDataRefType,
                              kQTFileTypeMovie, FOUR_CHAR_CODE('TVOD'),
                              createMovieFileDeleteCurFile |
                              createMovieFileDontCreateResFile,
                              ci);
    }

Done:

    DetachMovieFromCurrentThread(worker->theMovie);

    if (ci) CloseComponent(ci);
    if (exportSettings) QTDisposeAtomContainer(exportSettings);
    if (theDataRef) DisposeHandle(theDataRef);

CantAttachToCurrentThread:

    ExitMoviesOnThread();

    worker->threadStatus = status;

    CreateEvent(NULL, kEventClassQTThreading, kEventAppCleanUpThreadDroppings, 0,
                kEventAttributeNone, &theEventRef);
    SetEventParameter(theEventRef, kEventParamThreadData, typeWorkerThreadRef,
                      sizeof(worker), &worker);

    if (theEventRef) {
        PostEventToQueue(worker->mainEventQueue, theEventRef,
                         kEventPriorityStandard);
        ReleaseEvent(theEventRef);
    }

    pthread_exit(NULL);
}

(g) ビデオをGWorldにレンダリングし、バックグラウンドスレッドでゼロからムービーを作成します。

リスト7では、新しいムービーとムービーファイルを作成して、移動したムービーを'2vuy' GWorldにレンダリングし、lumaを操作して、バックグラウンドスレッドで新たに作成したムービーファイルにサンプルデータを保存します。

リスト7:バックグラウンドスレッドでムービーを作成してレンダリング

// このworker関数はバックグラウンドスレッドで新しいムービーを作成してレンダリングする
void *DoSomeWorkFunction(void *inWorkerThreadRef)
{
    Handle theDataRef;
    OSType theDataRefType;
    DataHandler theDataHandler = 0;
    Movie theNewMovie = NULL;
    ImageDescriptionHandle id;
    GWorldPtr theGWorld;
    PixMapHandle theGWorldPixMap;
    unsigned long theGWorldRowBytes;
    Ptr baseAddr;
    long theDataSize;
    Track theTrack;
    Media theMedia;
    Rect srcRect;
    short flags;
    OSType whichMediaType = VideoMediaType;
    TimeValue movieTime = 0;
    TimeValue duration;
    EventRef theEventRef = NULL;
    OSStatus status;

    WorkerThreadRef worker = (WorkerThreadRef)inWorkerThreadRef;

    EnterMoviesOnThread(0);

    status = AttachMovieToCurrentThread(worker->theMovie);
    require_noerr(status, CantAttachToCurrentThread);

    CFStringRef path = CFSTR("/Users/fasteddie/Desktop/testoutput.mov");

    status = QTNewDataReferenceFromFullPathCFString(path,
                                                    kQTNativeDefaultPathStyle,
                                                    0,
                                                    &theDataRef,
                                                    &theDataRefType);
    require_noerr(status, Done);

    status = CreateMovieStorage(theDataRef, theDataRefType,
                                FOUR_CHAR_CODE('TVOD'), smSystemScript,
                                createMovieFileDeleteCurFile |
                                createMovieFileDontCreateResFile,
                                &theDataHandler, &theNewMovie);
    require_noerr(status, Done);

    // ムービーのサイズを取得する
    GetMovieBox(worker->theMovie, &srcRect);
    MacOffsetRect(&srcRect, -srcRect.left, -srcRect.top);

    // フレームをレンダリングするGWorldを作成する
    status = QTNewGWorld(&theGWorld,
                         k2vuyPixelFormat,
                         &srcRect,
                         NULL,
                         NULL,
                         0);
    require_noerr(status, Done);

    // GWorldを設定する
    SetGWorld(theGWorld, NULL);
    SetMovieGWorld(worker->theMovie, theGWorld, NULL);

    theGWorldPixMap = GetGWorldPixMap(theGWorld);
    LockPixels(theGWorldPixMap);
    baseAddr = GetPixBaseAddr(theGWorldPixMap);
    theGWorldRowBytes = QTGetPixMapHandleRowBytes(theGWorldPixMap);

    // 画像記述とデータサイズを取得する
    // 注記:「流氷通信19」に従って、
    // 非圧縮のY'CbCrビデオデータを含むQuickTimeムービーファイルの画像記述は
    // バージョン2にして、必要な画像記述の拡張をいくつかのインクルードする。
    // シンプルにするために、これらは追加しない。
    // http://developer.apple.com/quicktime/icefloe/dispatch019.html
    status = MakeImageDescriptionForPixMap(theGWorldPixMap, &id);
    theDataSize = (**id).dataSize;
    
    // 新しいムービートラックとメディアを作成する
    theTrack = NewMovieTrack(theNewMovie,
                             FixRatio((srcRect.right - srcRect.left), 1),
                             FixRatio((srcRect.bottom - srcRect.top), 1),
                             kNoVolume);
    status = GetMoviesError();
    require_noerr(status, Done);

    theMedia = NewTrackMedia(theTrack, VideoMediaType,
                             kQTSDefaultMediaTimeScale, NULL, 0);
    status = GetMoviesError();
    require_noerr(status, Done);

    // サンプルデータをファイルに書き出すための編集セッションを開始する
    status = BeginMediaEdits(theMedia);
    require_noerr(status, Done);

    // 最初のフレームに、現在のフレームをインクルードする
    flags = nextTimeMediaSample | nextTimeEdgeOK;
    whichMediaType = VideoMediaType;

    Handle theSampleData = NewHandle(theDataSize);
    if (MemError() || NULL == theSampleData) goto Done;

    while (1) {
        // ソースムービーの次のフレームを取得する
        // 次に対象とする時間にスキップする
        GetMovieNextInterestingTime(worker->theMovie,
                                    flags,
                                    1,
                                    &whichMediaType,
                                    movieTime,
                                    0,
                                    &movieTime,
                                    &duration);
        status = GetMoviesError();
        require_noerr(status, Done);

        if (-1 == movieTime) break;

        // フレームの時間を設定する
        SetMovieTimeValue(worker->theMovie, movieTime);

        // フレームをGWorldに描画する
        MoviesTask(worker->theMovie, 0);
        status = GetMoviesError();
        require_noerr(status, Done);

        // lumaを調整して、それぞれのY値から25を引き、
        // 最小値を16に固定する(25+16 = 41)
        // CbYCrY 8ビット、各コンポーネントのピクセル 0-1
        // 「流氷通信19」を参照
        // http://developer.apple.com/quicktime/icefloe/dispatch019.html
        UInt32 height = srcRect.bottom;
        Ptr nextScanLine = baseAddr;
        while (height--) {
            UInt32 width = theGWorldRowBytes >> 1;
            UInt8 *thePixPtr = (UInt8 *)nextScanLine;
            while (width--) {
                UInt8 Y = thePixPtr[1];
                Y = ((Y <= 41) * 16) | ((Y - 25) * !(Y <= 41));
                thePixPtr[1] = Y;
                thePixPtr += 2;
            }
            nextScanLine += theGWorldRowBytes;
        }

        // メディアサンプルをムービーに追加する
        PtrToXHand(baseAddr, theSampleData, theDataSize);
        status = AddMediaSample(theMedia,
                                theSampleData, // the video sample
                                0,             // no offset into data
                                theDataSize,
                                60,            // frame duration
                                (SampleDescriptionHandle)id,
                                1,             // one sample
                                0,             // self-contained samples
                                NULL);
        require_noerr(status, Done);

        flags = nextTimeMediaSample;
    }

    // メディア編集セッションを終了する
    status = EndMediaEdits(theMedia);
    require_noerr(status, Done);

    // メディアをトラックに追加する
    status = InsertMediaIntoTrack(theTrack, 0, 0, GetMediaDuration(theMedia),
                                  fixed1);
    require_noerr(status, Done);

    status = AddMovieToStorage(theNewMovie, theDataHandler);

Done:

    SetMovieGWorld(worker->theMovie, NULL, NULL);

    // 何かが混乱しているため、ファイルを削除する
    if (status && theDataHandler) {
        DataHDeleteFile(theDataHandler);
    }

    if (theDataRef) DisposeHandle(theDataRef);
    if (theDataHandler) CloseMovieStorage(theDataHandler);
    if (id) DisposeHandle((Handle)id);
    if (theSampleData) DisposeHandle(theSampleData);
    if (theNewMovie) DisposeMovie(theNewMovie);
    if (theGWorld) DisposeGWorld(theGWorld);

    DetachMovieFromCurrentThread(worker->theMovie);

CantAttachToCurrentThread:

    worker->threadStatus = status;

    ExitMoviesOnThread();

    CreateEvent(NULL, kEventClassQTThreading, kEventAppCleanUpThreadDroppings,
                0, kEventAttributeNone, &theEventRef);
    SetEventParameter(theEventRef, kEventParamThreadData, typeWorkerThreadRef,
                      sizeof(worker), &worker);

    if (theEventRef) {
        PostEventToQueue(worker->mainEventQueue, theEventRef,
                         kEventPriorityStandard);
        ReleaseEvent(theEventRef);
    }

    pthread_exit(NULL);
}

リスト8:Do_SomeWorkOnSeparateThread

// お勧めのWorkerスレッド構造
typedef struct {
    SInt32        refCount;
    pthread_t     workerThread;
    Movie         theMovie;
    EventQueueRef mainEventQueue;
    OSStatus      threadStatus;
} WorkerThread, *WorkerThreadRef;

// カスタムのCarbonイベントタイプ
enum {
  kEventClassQTThreading = 'QTTH',
  kEventAppCleanUpThreadDroppings = 'clup',
  kEventParamThreadData = 'thrd',
  typeWorkerThreadRef = 'thrd'      // WorkerThreadRef
};


// Carbonイベントハンドラをインストールするのに必要なコード
// このコードは、場合によりRunApplicationEventLoop()を呼び出す前に、
// 最小セットのCarbonイベントハンドラをインストールする
// RunApplicationEventLoop()

...

EventTypeSpec eventType[] = {{kEventClassQTThreading,
                              kEventAppCleanUpThreadDroppings}};
status = InstallApplicationEventHandler(Handle_CleanUpThreadData,
                                        GetEventTypeCount(eventType),
                                        eventType, NULL, NULL);

...

// カスタムのクリーンアップイベントのCarbonイベントハンドラ
static pascal OSStatus Handle_CleanUpThreadData(EventHandlerCallRef inHandlerCallRef,
                                                EventRef inEvent, void *inUserData)
{
    WorkerThreadRef worker = NULL;

    GetEventParameter(inEvent, kEventParamThreadData, typeWorkerThreadRef,
                      NULL, sizeof(worker), NULL, &worker);
    if (NULL == worker) return eventNotHandledErr;

    if (1 == DecrementAtomic(&worker->refCount)) {
        DisposeMovie(worker->theMovie);
        free(worker);
    }

    return noErr;
}

// コンポーネントにcmpThreadSafeフラグがあるかどうかチェックする
Boolean IsComponentThreadSafe(OSType inComponentType, OSType inComponentSubType)
{
    Component comp = 0;
    ComponentDescription cd = { inComponentType, inComponentSubType,
                                kAnyComponentManufacturer, 0, cmpIsMissing };
    ComponentDescription compDesc;

    comp = FindNextComponent(0, &cd);
    while (comp != NULL) {
        GetComponentInfo(comp, &compDesc, NULL, NULL, NULL);
        if (compDesc.componentFlags & cmpThreadSafe) return true;

        comp = FindNextComponent(comp, &cd);
    }

    return false;
}

/*****************************************************
*
* Do_SomeWorkOnSeparateThread(WindowRef aWindow)
*
* 目的:渡されたウインドウで再生されているムービーをタスク化し、
* コピーして、バックグラウンドスレッドに移動し、
* 上記のDoSomeWorkFunctionsのいずれかを呼び出して作業を実行する
*
* 入力:ウインドウ参照
*
* 戻り値:なし
*/
static OSStatus Do_SomeWorkOnSeparateThread(WindowRef aWindow)
{
    WorkerThreadRef worker;
    pthread_attr_t attr;
    OSStatus status = paramErr;

    if (NULL == aWindow) return status;

    WindowDataPtr wdr = (WindowDataPtr)GetWRefCon(aWindowRef);
    if (NULL == wdr) return status;

    Handle cloneHandle = NewHandle(0);
    if (NULL == cloneHandle || status = MemError()) return status;

    // workerスレッドデータにメモリを割り当てる
    worker = calloc(1, sizeof(WorkerThread));
    if (NULL == worker) { status = memFullErr; goto Failure; }

    // バックグラウンドスレッドからPostEventToQueueを呼び出せるように
    // メインイベントキューが必要だが、
    // GetMainEventQueueは10.3.xではスレッドセーフでないため、
    // ここでこれを設定する
    // ただし、GetMainEventQueueは10.4以降では
    // スレッドセーフであるため、新しいシステム上では
    // あとでこれを柔軟に設定できる
    worker->mainEventQueue = GetMainEventQueue();

    // オリジナルのムービーをコピーして、その新しいムービーをメインスレッドに
    // アタッチし、間違いなくデタッチする
    status = PutMovieIntoHandle(wdr->fMovie, cloneHandle);
    require_noerr(status, Failure);

    status = NewMovieFromHandle(&worker->theMovie, cloneHandle, newMovieActive, NULL);
    require_noerr(status, Failure);

    status = DetachMovieFromCurrentThread(worker->theMovie);
    if (componentNotThreadSafeErr == status) {

        // しまった!このムービーは別のスレッドにあるので、エクスポートできない
        // まず、安全でないメディアを含んでいるトラックを削除する必要がある

        long trackCount = GetMovieTrackCount(worker->theMovie);
        long count;

        // 安全でないタイプのトラックをすべて削除する
        for (count = 1; count <= trackCount; count++) {
            Track track = GetMovieIndTrack(worker->theMovie, count);
            Media media = GetTrackMedia(track);
            SampleDescriptionHandle desc;
            OSType theMediaType;
            OSType theCodecType;

            if (track) {
                GetMediaHandlerDescription(media, &theMediaType, NULL, NULL);
                desc = (SampleDescriptionHandle)NewHandle(0);
                GetMediaSampleDescription(media, 1, desc);
                theCodecType = (**desc).dataFormat;
                DisposeHandle((Handle)desc);
                if (!IsComponentThreadSafe(MediaHandlerType, theMediaType) ||
                    !IsComponentThreadSafe(decompressorComponentType, theCodecType)) {
                    DisposeMovieTrack(track);
                    count--;
                }
            }
        }

        // トラックカウントを再度取得する - トラックが残っていない場合は、
        // エクスポートを試みてもあまり意味はない
        trackCount = GetMovieTrackCount(worker->theMovie);
        if (trackCount == 0) goto Failure;

        // 再試行する
        status = DetachMovieFromCurrentThread(worker->theMovie);
        require_noerr(status, Failure);
    }

    // スレッドをデタッチする
    pthread_attr_init(&attr);
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);

    IncrementAtomic(&worker->refCount);

    // workerスレッドを作成し、何か作業をする
    status = pthread_create(&worker->workerThread, &attr, DoSomeWorkFunction, worker);
    pthread_attr_destroy(&attr);
    require_noerr(status, Failure);

    DisposeHandle(cloneHandle);

    return noErr;

Failure:
    if (cloneHandle) DisposeHandle(cloneHandle);
    if (worker->theMovie) DisposeMovie(worker->theMovie);
    if (worker) free(worker);
    return status;
}

先頭に戻る

コンポーネントデベロッパへのアドバイス

QuickTimeの高度なモジュール設計では、新しいサービスや特定のサービスを提供する新しいコンポーネントを追加することで拡張が可能です。

コンポーネントをアップデートする開発者の方は、次のバージョンをスレッドセーフにしてください。そのために必要な労力は、各コンポーネントのタイプによって異なります。しかし、すべてのコンポーネントタイプに当てはまる一般的なガイドラインがいくつかあります。

1回限りのComponent ManagerリソースAPIの使用

前述のように、Resource Managerはスレッドセーフではありませんが、コンポーネントリソースを返す1回限りのComponent Manager呼び出しはスレッドセーフです。ImageCodecGetCodecInfoGraphicsImportGetMIMETypeListなど、コンポーネントリソースに保存された情報を返すコンポーネント呼び出しを実装する場合は、必要に応じて1回限りのComponent ManagerリソースAPIを使用します。リスト9~11を参照してください。

リスト9:悪い例 - Resource Managerを使用しない

// 下記の例は、コンポーネントがリソースの1つを取得するのに使用できない典型的な
// 古いコードである。これは比較的コード量が多く、スレッドセーフではない。
// コンポーネントにこのような古いコードが含まれている場合は、
// コードをアップデートしたほうがよい。

OSErr  err;
Handle resource = NULL;
short  saveResFile;
short  resRef;

saveResFile = CurResFile();
err = OpenAComponentResFile((Component)store->self, &resRef);
if (err == noErr) {
  resource = Get1Resource('PICT', 128);
  if (resource) {
    LoadResource(resource);
    DetachResource(resource);
  } else {
    err = ResError();
    if (err == noErr)
      err = resNotFound;
  }

  CloseComponentResFile(resRef);
  UseResFile(saveResFile);
}

リスト10:正しい例 - GetComponentResourceを使用

// Component Managerの1回限りのスレッドセーフ機能を使用する。
// この1行は、上記のリストとまったく同じ機能を実行する。

OSErr  err;
Handle resource = NULL;

err = GetComponentResource((Component)store->self, 'PICT', 128, &resource);

リスト11:正しい例 - ImageCodecGetInfoを実装

// 'cdci' リソースの例
#define kMyCodecFormatName  "My Cool Codec"

// これらのフラグは、コンポーネントの機能に関する情報を指定する
#define kMyDecoFlags (codecInfoDoes32 | codecInfoDoes8)

// これらのフラグは、コンポーネントが生成できる圧縮データのフォーマットと
// コンポーネントが解凍時に処理できる圧縮ファイルのフォーマットを指定する
#define kMyFormatFlags (codecInfoDepth32 | codecInfoDepth40)

// コンポーネントの説明
resource 'cdci' (129) {
    kMyCodecFormatName, // Type
    1,                  // Version
    0,                  // Revision level
    'MINE',             // Manufacturer
    kMyDecoFlags,       // Decompression Flags
    0,                  // Compression Flags
    kMyFormatFlags,     // Format Flags
    0,                  // Compression Accuracy
    128,                // Decomression Accuracy
    0,                  // Compression Speed
    200,                // Decompression Speed
    0,                  // Compression Level
    0,                  // Reserved
    8,                  // Minimum Height
    8,                  // Minimum Width
    0,                  // Decompression Pipeline Latency
    0,                  // Compression Pipeline Latency
    0                   // Private Data
};

// ImageCodecGetCodecInfo
// アプリケーションがImage Compression ManagerのGetCodecInfo関数を呼び出すときは
// いつでも、コーデックがImageCodecGetCodecInfo要求を受け取る。
// コンポーネントは、その機能を定義するフォーマット済みの
// CodecInfo構造を返す。
// compressorとdecompressorはこの呼び出しを受け取ることができる。
pascal ComponentResult MyDeco_GetCodecInfo(MyCodecGlobals glob, CodecInfo *info)
{
    CodecInfo **tempCodecInfo;
    OSErr err = noErr;

    if (NULL == info) return paramErr;

    err = GetComponentResource((Component)glob->self, codecInfoResourceType,
                               129, (Handle *)&tempCodecInfo);
    if (noErr == err) {
        *info = **tempCodecInfo;
        DisposeHandle((Handle)tempCodecInfo);
    }

    return err;
}

これらのAPIに関する詳細は、「流氷通信 21」 を参照してください。

先頭に戻る

グローバル状態を保存するのにComponent RefConを使用しない

一部のコンポーネントは、コンポーネントインスタンス間で共有されているグローバル状態を維持しており、そのためにComponent RefConを使用しています。これは、複数のスレッドからコンポーネントを呼び出すときには機能しなくなります。

コンポーネントがSetComponentRefConGetComponentRefConを使用している場合は、この使用を見直す必要があります。コンポーネント間の通信に使用する共有グローバルは保護する必要があります。また、定数テーブルの共有グローバルは、このドキュメントの次のセクションで述べるように、グローバル定数データと置き換える必要があります。

コンポーネント間の動的状態を共有する必要がある場合は、標準的なロック手法を利用して、このグローバル状態へのアクセスを通常どおり保護する必要があります。グローバル状態の初期化を防ぐには、pthread_onceなどの関数を使用します。

リスト12:pthread_once関数

int pthread_once(pthread_once_t *once_control, void (*init_routine)(void))

pthread_onceは、初期化コードが一度だけ実行されたことを確認して、1回限りの初期化を実行する。

once_control - PTHREAD_ONCE_INITに初期化する静的変数のポインタ。

init_routine - 次のプロトタイプに対応するCの初期化関数、
void initRoutine(void);

ディスカッション:

pthread_onceを所定のonce_control引数で最初に呼び出すと、この関数はinit_routineを呼び出し、
初期化が成功したことを記録するonce_controlの値を設定する。成功した初期化が何も実行
しないうちに、同じonce_control引数でpthread_onceを呼び出す。

リスト13:pthread_onceを初期化に使用

#include <pthread.h>

typedef struct  {
    Handle  someGlobalHandle;
    long    someDynamicValues[ARRAY_SIZE];
    ...
} SharedGlobals;

typedef struct {
    ComponentInstance   self;
    ComponentInstance   delegateComponent;
    ComponentInstance   target;
    OSType              codecType;
    SharedGlobals       *sharedGlob;
    ...
} ComponentGlobals, *ComponentGlobalsPtr;

static SharedGlobals componentSharedGlobals;

static void InitSharedGlobals(void)
{
    componentSharedGlobals.someGlobalHandle = NewHandle(HANDLE_SIZE * sizeof(long));

    InitSomeGlobalHandle(&componentSharedGlobals);
    InitSomeDynamicValues(&componentSharedGlobals);
}

static SharedGlobals* GetSharedGlobals(void)
{
    static pthread_once_t control = PTHREAD_ONCE_INIT;

    /* pthread_onceは1回のプロセスで一度だけルーチンを実行する。
       このプロセスで任意のスレッドがこのサブルーチンを初めて呼び出すと
       パラメータなしで所定のルーチンを実行する。
       その後の呼び出しは効果がない。

       これが役に立つのは、固有の初期化を1つのスレッドで実行する必要があり、
       同期要件を緩和する場合である。*/

    pthread_once(&control, InitSharedGlobals);

    return &componentSharedGlobals;
}

pascal ComponentResult MyCodec_ImageCodecOpen(ComponentGlobalsPtr glob,
                                              ComponentInstance self)
{
    ComponentResult result = noErr;
    ComponentDescription cd;

    result = GetComponentInfo((Component)self, &cd, NULL, NULL, NULL);
    if (result) return result;

    if ((glob = (ComponentGlobalsPtr)calloc(1, sizeof(ComponentGlobals))) == NULL)
        return memFullErr;

    SetComponentInstanceStorage(self,(Handle)glob);

    glob->self = self;
    glob->codecType = cd.componentSubType;
    ...
    glob->sharedGlob = GetSharedGlobals();

    ...

    return result;
}

先頭に戻る

定数テーブルを実行可能形式に置く

コンポーネントがいくつかの定数テーブルの共有グローバルを必要とする場合(たとえば共有のグローバルを一度割り当て、後で使用するために保存する必要がある場合)は、テーブルを別々に作成し、事前に作成したデータをstatic constとしてラベル付けされたコンポーネント実行可能形式に配置することを検討します。

これは有用な手法で、いくつかのメリットがあります。

  • テーブルデータが定数で、コンパイラがそのことを認識している場合は、データがコンポーネント実行可能形式の読み取り専用セクションに置かれます。これはTEXTセグメントとも呼ばれています。

    テキストセグメントは、コンポーネント実行可能形式と同じ場所です。これらの定数テーブルは単一アプリケーション内にあるコンポーネントの複数インスタンスで共有できるだけでなく、複数アプリケーションのコンポーネントの複数インスタンスでも共有できます。定数データは、物理的なメモリに一度だけは存在することになります。

  • このテーブルデータは、読み取り専用のマークが付けられます。メモリをページアウトする必要がある場合、システムは読み取り専用ファイルからマッピングすることによって、テーブルデータをいつでも元に戻せることを認識しています。テーブルデータを無駄にディスクに書き出すことがないため、これは長所です。

リスト14:事前に作成したテーブルを定数としてマーク

static const gReverseBitsTable[256] = {
    0x00, 0x80, 0x40, 0xc0, 0x20, 0xa0, 0x60, 0xe0,
    0x10, 0x90, 0x50, 0xd0, 0x30, 0xb0, 0x70, 0xf0,
    0x08, 0x88, 0x48, 0xc8, 0x28, 0xa8, 0x68, 0xe8,
    0x18, 0x98, 0x58, 0xd8, 0x38, 0xb8, 0x78, 0xf8,
    [..]
};

ターミナルから nm -m を実行すると、これらのテーブルが確かに
TEXTセグメントに置かれている様子が表示される。

% nm -m whatever.o
00000000 (__TEXT,__const) non-external _gReverseBitsTable

テーブル定義に"const"キーワードを使用しなかった場合は、
テーブルがDATAセグメントに置かれ、各プロセスが
それぞれの書き込み可能コピーを取得する。

00000000 (__DATA,__data) non-external _gNonConstReverseBitsTable

先頭に戻る

コンポーネントスレッドセーフフラグの設定

コンポーネントをスレッドセーフにしたら、グローバルコンポーネントフラグでコンポーネントスレッドセーフフラグを必ず設定してください。これはQuickTime(またはコンポーネント情報を要求するアプリケーション)に、コンポーネントがバックグラウンドスレッドから安全に使用できることを示します。

リスト15:コンポーネントスレッドセーフフラグ

cmpThreadSafe  = 1L << 28 // コンポーネントはスレッドセーフ

リスト16:cmpThreadSafeフラグの追加

// 拡張'thng'テンプレート
#define thng_RezTemplateVersion 1

#include <Carbon/Carbon.r>
#include <QuickTime/QuickTime.r>

resource 'thng' (256) {
    kSomeQTComponentType, // タイプ
    'DEMO',               // サブタイプ
    'DEMO',               // メーカー
    0,
    0,
    0,           // - componentHasMultiplePlatformsを使用
    0,
    'STR ',      // 名前タイプ
    128,         // 名前 ID
    'STR ',      // 情報タイプ
    129,         // 情報 ID
    0,           // アイコンタイプ
    0,           // アイコン ID
    kMyComponentVersion,  // バージョン
    // 登録フラグ
    componentHasMultiplePlatforms |
    kOtherComponentRegistrationFlags,
    0,           // アイコンファミリのリソースID
    {
      // スレッドセーフフラグを追加
      kMyComponentFlags | cmpThreadSafe,
      'dlle',    // コードリソースタイプ - 発見されたエントリポイント
                 // シンボル名'dlle'リソース
      512,       // 'dlle'リソースのID
      platformPowerPCNativeEntryPoint,
                 // プラットフォームタイプ(gestaltComponentPlatform 
                 // からの応答、あるいは、この応答がない場合は
                 // gestaltSysArchitecture)
    };
};

'thng'リソースとMach-Oコンポーネントのビルドに関する詳細は、テクニカルノートのTN2012「Building Universal QuickTime Components for Mac OS X」を参照してください。

先頭に戻る

QuickTimeスレッドセーフコンポーネント

QuickTime 6.5.2がインストールされているMac OS X 10.3以降では、以下に示すQuickTimeコンポーネントがスレッドセーフです。QuickTimeのバージョンが新しいほど、スレッドセーフなコンポーネントもより多く提供されています。Fiendishthngs(このドキュメントの「ツール」セクション参照)を使って、より新しいバージョンのQuickTimeでの特定のコンポーネントの機能を調べることができます。

重要:DVオーディオデコーダを除いて、Sound Managerのオーディオエンコーダおよびデコーダはすべて、バージョン6.4~6.5.2のQuickTimeではスレッドセーフではありません。バックグラウンドスレッドへ移行する前にムービーのサウンドトラックを削除し、バックグラウンドスレッド上にオーディオを含むムービーファイルをインポートしないようにする必要があります。

最新バージョンのQuickTimeではCore Audioフレームワークを使い、これらの制限の多くを取り除いています。

表1:

Image Codecs

'imco' compressorComponentType

'imdc' decompressorComponentType
Image Decompressors

'imdc' decompressorComponentType
Apple DV/DVCPRO Codec - NTSCApple Base(別名 Base Codec)
Apple DV - PAL CodecApple CMYK
Apple DVCPRO - PAL CodecApple GIF
Apple DVCPRO50 - NTSC CodecApple AVR JPEG
Apple DVCPRO50 - PAL CodecApple OpenDML JPEG
Apple Planar RGB CodecApple r408
Apple PNG CodecApple Scaling
Apple None Codec(別名 Raw Codec)Apple Sorenson YUV9
Apple Animation CodecApple YUV420
Apple Video CodecApple YUV422
Apple TIFF CodecApple 16-bit Grey
Apple Cinepak CodecApple 32-bit Grey with Alpha
Apple Photo - JPEG CodecApple 48-bit RGB
Apple Motion JPEG A CodecApple 64-bit ARGB
Apple Motion JPEG B Codec 
JPEG 2000 Codec 
Apple MPEG4 Codec 

表2:

Still Image Importers & Exporters

'grip' GraphicsImporterComponentType

'grex' GraphicsExporterComponentType
Still Image Importers

'grip' GraphicsImporterComponentType
JPEGGIF
PhotoshopJPEG 2000
PNG 
QuickTime Image 
TIFF 
Base Import / Export 

表3:

Movie Importers & Exporters

'eat ' MovieImportType

'spit' MovieExportType
Movie Importers

'eat ' MovieImportType
Movie Exporters

'spit' MovieExportType
DVAnimated GIF3G
QuickTime MovieSlow MovieBMP
MPEG-4Compact Disk Audio (AIFF)Image Sequence
AVI  
AIFF  
WAVE  
muLaw  

表4:

Data Handlers

'dhlr' DataHandlerType
Media Handlers

'mhlr' MediaHandlerType
Apple Handle Data HandlerApple Generic Media Handler
Apple NULL Data HandlerApple MPEG4 ODSM Media Handler
Apple Pointer Data HandlerApple MPEG 4 Scene Media Handler
Apple Resource Data HandlerApple Standard Media Handler
Apple Alias Data HandlerTime Code Media Handler
 Apple Video Media Handler

表5:

Standard Compression

'scdi' StandardCompressionType
Data Codecs

'dcom' DataCompressorComponentType

'ddec' DataDecompressorComponentType
Standard ImageApple Data
Standard SoundDeflate / Inflate (zlib)

表6:

Sound Decompressors

'sdec' kSoundDecompressor
Clock Components

'clok' clockComponentType
DV Sound DecompressorApple Microsecond Clock

先頭に戻る

QuickTime for Windows

比較的安全なWindows上では1つのバックグラウンドスレッドからQuickTimeを呼び出すことができますが、複数のバックグラウンドスレッドからQTML (QuickTime Media Layer)を呼び出すのに必要なシステムレベルのサポートはWindowsプラットフォームにはありません。たとえば、QuickDrawのような重要なフレームワークはWindowsではスレッドセーフではありません。

さらに、Windows上ではQuickTimeは完全にシリアライズされており、QuickTimeライブラリへの呼び出しができるのは、一度に1つのスレッドに限られています。あるスレッドがQuickTime処理を実行している場合、QuickTimeのAPIを呼び出しているほかのスレッドは、前の処理が戻ってくるまでブロックされます。

そのため、Windowsで複数のバックグラウンドスレッドからQuickTimeを呼び出すのはお勧めしません。

注:これは、QuickTime for Windows 3.0のリリース以来、 QuickTime for Windowの現在のバージョンまで変わらず残っている問題です。

重要:このテクニカルノートで説明しているスレッドセーフなAPIは、QuickTime for Windows SDKを使用する場合には当てはまりません。

先頭に戻る

参考資料

技術書一覧

先頭に戻る

Mac OS X

先頭に戻る

PThreadsとマルチスレッディング

先頭に戻る

ツール

Fiendishthngs(Mac OS Xユニバーサルバイナリ) - このユーティリティを使うと、システム上にあるすべてのコンポーネントを一覧表示し、ほとんどのタイプのQuickTimeコンポーネントの詳細情報を照会できます。

先頭に戻る

ドキュメント改訂履歴

日付メモ
2006-02-14QuickTime for Windowsのセクションを追加
2005-05-05編集
2004-10-08アプリケーションデベロッパおよびコンポーネントデベロッパが、バックグラウンドスレッドからQuickTimeを使う方法を説明

掲載日: 2006-02-14




Did this document help you?
Yes: Tell us what works for you.

It’s good, but: Report typos, inaccuracies, and so forth.

It wasn’t helpful: Tell us what would have helped.