VHS(またはVHS-C)からBD(またはDVD)作成【7】〜書き込み(PowerDirector編)〜

PowerDirectoryでのフォルダ作成の方法を説明します。

  1. ディスク作成タブを選択し、「〜で書き込み」をクリックする。
    PowerDirector書き込み入口
  2. 「フォルダーの作成」にチェックを入れ、「フォルダーディレクトリー」にフォルダの作成先を指定し、「書き込み開始」をクリックする。
    PowerDirector書き込み開始

これで、PowerProducer編と同様に、
指定したフォルダに
BDの場合は、「BDMV」と「CERTIFICATE」
DVDの場合は、「AUDIO_TS」と「VIDEO_TS」
が作成されます。

後は、PowerProducer編と同様に、メディアに書き込むだけです。

お疲れさまでした。

VHS(またはVHS-C)からBD(またはDVD)作成【6】〜書き込み(PowerProducer編)〜

編集した内容をBD(またはDVD)へ書き出す作業を行います。

まず、お手持ちのディスクドライブがBDやDVDの書き込みに対応しているか確認してください。
私の場合BDへの書き込みのため、今回、

BUFFALO BRXL-PC6VU2-BKC

を購入しました。

BD-Rについては、大事なデータですので、口コミ情報で信頼性が高そうな、
パナソニック LM-BRS25LT50

を購入しました。

さて、書き込みフェーズについても、PowerProducerとPowerDirectorで異なりますので、それぞれについて紹介します。
まずは、PowerProducerでの書き込みについて紹介します。

編集フェーズから、「次へ」を選択すると、
編集後、次へ
書き込み設定画面へ遷移します。

フォルダ作成

このフェーズから直接ディスクへ書き込むこともできるのですが、
一旦ハードディスクに書き込む内容を保存する方が確実です。
「ディスクの書き込み」のチェックを外し、「フォルダーの作成」へチェックを入れておきます。
DVDの場合は「ディスクイメージの保存」ができるのですが、BDではできないのでここでは説明しません。

「書き込み」を選択すると、指定したフォルダに
BDの場合は、「BDMV」と「CERTIFICATE」
DVDの場合は、「AUDIO_TS」と「VIDEO_TS」
が作成されます。

作成されたフォルダをメディアで書き込む手順はPowerProducerもPowerDirectorも同じです。
PowerDirectorでのフォルダ作成方法を説明する前に、先にメディアに書き込む方法を説明してしまいます。

フリーソフトのimgBurnを用いて、メディアに書き込むことができます。
imgBurnの設定でオプションタブを選択し、

DVDの場合は、
DVD オプション

BDの場合は、
BD オプション

を指定します。
ただし、imgBurnは2層メディアには弱いらしいです。

2層メディアの場合は、BDドライブのBRXL-PC6VU2-BKCにバンドルされていたPower2Goで書き込みを行うようにします。
もちろん1層も問題なく書き込めるので、BRXL-PC6VU2-BKC購入した場合なら、Power2Go一択で問題ありません。

  1. 「データディスク」を選択し、「DVD」or「BD」を選択する。
    データディスク作成
  2. 「設定」を選択し、「データ」タブ内の「ファイルシステム」から適切なUDFリビジョンを選択(DVDの場合2.0、BDの場合2.5)
    ファイルシステム選択
  3. 書き込むフォルダを上のフレームから下のフレームへドラッグアンドドロップし、「書き込み」します。
    書き込み

次はPowerDirectoryでのフォルダ作成の方法を説明します。

VHS(またはVHS-C)からBD(またはDVD)作成【5】〜タイトルメニュー(PowerDirector編)〜

今回はPowerDirectorでテンプレートを作成して、利用する方法を紹介します。

  1. 「フル機能エディター」を選択する。
    フル機能エディターへ
  2. 「ディスク作成」タブを選択し、「メニューの環境設定」タブを選択し、「メニュー作成」を選択する。
    PowerDirectorメニュー作成
  3. メニューデザイナーが起動する。
    メニューデザイナー

メニューデザイナーは直感的に操作ができるので、ここでの詳細の説明は省略します。
代わりに、CyberLinkの公式ページのチュートリアルを提示しておきます。

ただ、このPowerDirectorで作成できるテンプレートにもかなり制限があり、細かいところに手が届かないところがありました。
ここでは、2点ほど気に入らなかった点を泥臭く解決したので、紹介しておきます。

まず、1点目。メニュー内で作成されるサムネイルに表示される映像は、元映像の内側をトリミングするような形で表示されます。
かなり内側をトリミングしてしまうので、見た目残念な場合があります。
例えば、
元映像
のような映像を普通にサムネイル化すると、
残念なサムネイル
となってしまいます。
このトリミング枠をもっと外側へ広げたい場合、作成したメニューが保存されている
【ドライブ名】:\Users\yo\Documents\CyberLink\Custom Menus\3.0\Menu_XXX
のframeフォルダのstd_frame.xmlを修正します。(XXXは通し番号)

<FRAME ID="2" image="FM25.png" image_16v9="FM25_16v9.png" mask="FM25M.png" mask_16v9="FM25_16v9M.png" StereoSource="MONOSCOPIC">
    <CONTROLPOINT ID="1" left="0.000000" top="0.000000"/>
    <CONTROLPOINT ID="2" left="1.000000" top="0.000000"/>
    <CONTROLPOINT ID="3" left="1.000000" top="1.000000"/>
    <CONTROLPOINT ID="4" left="0.000000" top="1.000000"/>
    <CONTROLPOINT_16v9 ID="1" left="0.000000" top="0.000000"/>
    <CONTROLPOINT_16v9 ID="2" left="1.000000" top="0.000000"/>
    <CONTROLPOINT_16v9 ID="3" left="1.000000" top="1.000000"/>
    <CONTROLPOINT_16v9 ID="4" left="0.000000" top="1.000000"/>
</FRAME>

この元コードを

<FRAME ID="2" image="FM25.png" image_16v9="FM25_16v9.png" mask="FM25M.msk" mask_16v9="FM25_16v9M.msk" StereoSource="MONOSCOPIC">
    <CONTROLPOINT ID="1" left="0.056250" top="0.168750" />
    <CONTROLPOINT ID="2" left="0.947500" top="0.168750" />
    <CONTROLPOINT ID="3" left="0.947500" top="0.831250" />
    <CONTROLPOINT ID="4" left="0.056250" top="0.831250" />
    <CONTROLPOINT_16v9 ID="1" left="0.056250" top="0.250000" />
    <CONTROLPOINT_16v9 ID="2" left="0.947500" top="0.250000" />
    <CONTROLPOINT_16v9 ID="3" left="0.947500" top="0.753750" />
    <CONTROLPOINT_16v9 ID="4" left="0.056250" top="0.753750" />
</FRAME>

のようにFRAMEタグのmask、mask_16v9属性値(.png->.msk)、CONTROLPOINTタグのleft、top属性値を修正します。
結果、
OKなサムネイル
のように改善されます。

2点目。シーン画面で表示されるナビゲーションボタンの選択状態の表現が、緑色のバーしかないため、緑背景時には選択状態が分かりにくくなってしまいます。
20140323_select_ng

この形状と色を変更する方法を見つけました。
先ほどのframeフォルダと同階層のpcbgフォルダ内のPCBG_InvincibleHL.pngが形状を担っています。
20140323_select_bar_ng
黒色のPNG画像ですので、所望の形状を黒色で描いて保存してください。
例えば
20140323_select_bar_ok

色はhighlightのstd_highlight.xmlの00CC66(緑)を適切なRGB色成分に書き換えて下さい。
例えば、黄色にするなら、

<?xml version="1.0" encoding="UTF-16"?>
  <HIGHLIGHTLIST>
    <HIGHLIGHT StereoSource="MONOSCOPIC" color3="000000" color2="000000" color1="000000" textColor="000000" textColor_grayscale="000000" bkcolor_grayscale="000000" bkcolor="000000" highlightIcon_grayscale="" highlightIcon="" image_grayscale_16v9="" image_16v9="" image_grayscale="HL1" image="" ID="1"/>
    <HIGHLIGHT StereoSource="MONOSCOPIC" color3="00FF00" color2="FFFF00" color1="00CC66" textColor="FF00FF" textColor_grayscale="FF00FF" bkcolor_grayscale="FFFFFF" bkcolor="FFFFFF" highlightIcon_grayscale="" highlightIcon="" image_grayscale_16v9="GHL25_16v9.png" image_16v9="GHL25_16v9.png" image_grayscale="GHL25.png" image="GHL25.png" ID="2"/>
    <HIGHLIGHT StereoSource="MONOSCOPIC" color3="00FF00" color2="FFFF00" color1="00CC66" textColor="FFFFFF" textColor_grayscale="FFFFFF" bkcolor_grayscale="FFFFFF" bkcolor="FFFFFF" highlightIcon_grayscale="square_shape.png" highlightIcon="square_shape.png" image_grayscale_16v9="" image_16v9="" image_grayscale="HL1_user.png" image="HL1_user.png" ID="3"/>
    <HIGHLIGHT StereoSource="MONOSCOPIC" color3="00FF00" color2="FFFF00" color1="FFFF00" textColor="FFFFFF" textColor_grayscale="FF00FF" bkcolor_grayscale="FFFFFF" bkcolor="FF00FF" highlightIcon_grayscale="GIcon.png" highlightIcon="Icon.png" image_grayscale_16v9="" image_16v9="" image_grayscale="HL1_user.png" image="HL1_user.png" ID="4"/>
</HIGHLIGHTLIST>

<?xml version="1.0" encoding="UTF-16"?>
  <HIGHLIGHTLIST>
    <HIGHLIGHT StereoSource="MONOSCOPIC" color3="000000" color2="000000" color1="000000" textColor="000000" textColor_grayscale="000000" bkcolor_grayscale="000000" bkcolor="000000" highlightIcon_grayscale="" highlightIcon="" image_grayscale_16v9="" image_16v9="" image_grayscale="HL1" image="" ID="1"/>
    <HIGHLIGHT StereoSource="MONOSCOPIC" color3="00FF00" color2="FFFF00" color1="FFFF00" textColor="FF00FF" textColor_grayscale="FF00FF" bkcolor_grayscale="FFFFFF" bkcolor="FFFFFF" highlightIcon_grayscale="" highlightIcon="" image_grayscale_16v9="GHL25_16v9.png" image_16v9="GHL25_16v9.png" image_grayscale="GHL25.png" image="GHL25.png" ID="2"/>
    <HIGHLIGHT StereoSource="MONOSCOPIC" color3="00FF00" color2="FFFF00" color1="FFFF00" textColor="FFFFFF" textColor_grayscale="FFFFFF" bkcolor_grayscale="FFFFFF" bkcolor="FFFFFF" highlightIcon_grayscale="square_shape.png" highlightIcon="square_shape.png" image_grayscale_16v9="" image_16v9="" image_grayscale="HL1_user.png" image="HL1_user.png" ID="3"/>
    <HIGHLIGHT StereoSource="MONOSCOPIC" color3="00FF00" color2="FFFF00" color1="FFFF00" textColor="FFFFFF" textColor_grayscale="FF00FF" bkcolor_grayscale="FFFFFF" bkcolor="FF00FF" highlightIcon_grayscale="GIcon.png" highlightIcon="Icon.png" image_grayscale_16v9="" image_16v9="" image_grayscale="HL1_user.png" image="HL1_user.png" ID="4"/>
  </HIGHLIGHTLIST>

のように書き換えます。
すると、
20140323_select_ok
のような感じになります。

これでそれなりに納得できるメニューを作成することができました。

次は、最後の手順、ディスクへの書き込みです。

VHS(またはVHS-C)からBD(またはDVD)作成【4】〜タイトルメニュー(PowerProducer編)

今回は、タイトルメニューについて紹介します。
タイトルメニューとは、

※サンプル映像にNHKクリエイティブ・ライブラリーを利用しています。
このようなものです。

PowerProducerPowerDirector
作成するBD(あるいはDVD)にタイトルメニューを設置することができ、
コンテンツへのアクセスが行いやすくなります。

タイトルメニューはテンプレートから作成します。
テンプレートは、

  • PowerProducerにプレインストール済みのテンプレート
  • CyberLinkのDirector Zoneというテンプレートダウンロードサイト
  • PowerDirectorによるテンプレート作成

などにより、入手できます。
プレインストール済みではあまりに選択肢がなさすぎるので、Director Zoneを覗いてみるべきだと思います。
それでもやはり納得するテンプレートがない場合は、PowerDirectorで作成することをおすすめします。

では、一通りの説明ができるように、

  • Director Zoneからテンプレートをダウンロードして、PowerProducerで利用する方法
  • PowerDirectorでテンプレートを作成して、利用する方法

この二つのストーリーを紹介します。

まず、Director Zoneからテンプレートをダウンロードして、PowerProducerで利用する方法です。

  1. 以前の投稿で紹介した動画取り込みを実施
  2. 「ディスク」画面にて「編集」の「メニュー」を選択する。
    メニュー設定へ
  3. 「メニューの設定」画面へ遷移するので、「追加テンプレートのダウンロード」を選択する。
    追加テンプレートのダウンロード
  4. Director Zoneがブラウザで起動するので(サインインして、)ログインし、所望のテンプレートを探してダウンロードする。(ダウンロードしたものはdzmという拡張子で保存されます。)
    Director Zonedzmアイコン
  5. ダウンロードしたものを実行する。
  6. 「メニューの設定」画面で「テンプレート」とを選択する。
    テンプレートへ
  7. ダウンロードしたテンプレートを選択する。(なければ、1度PowerProducerを再起動してみて下さい。)
    テンプレートを選択

テンプレートを決めた後は、BGMや文字列の内容や位置、色、フォント、また、サムネイルの位置変更が可能です。

メニューは階層になっていますので、階層を遷移しながら、各画面に設定を行う必要があります。
トップ画面からサムネイルの画面へ遷移するには「シーン」を選んで、「決定」をクリックします。
サムネイルの画面からは、「次のページ」「前のページ」「メインへ戻る」を使って、各画面へ遷移します。
メニュー間移動

最後に「OK」で完了です。

次回は、PowerDirectorでテンプレートを作成して、利用する方法を紹介します。

VHS(またはVHS-C)からBD(またはDVD)作成【3】〜DVDからPCへの取り込み(PowerDirector編)〜

今回は、
PowerDirector

での取り込み〜編集を紹介します。

ダビングしたDVDをPCが認識できる状態で、PowerDirectorを起動します。

  1. 「フル機能エディター」を選択する。
    フル機能エディターへ
  2. 「キャプチャ」タブを選択。
  3. すぐ右下の「外部または光学デバイスからキャプチャー」タブを選択。
  4. 画面右下「ドライブ」でDVDドライブを選択。
  5. 「フォルダ変更」で取り込んだ動画の保存場所を指定
  6. 「プロファイル」で取り込み形式「MPEG2」を指定
  7. 「ムービー」まるごと取り込むか、タイトルまるごと取り込むか、チャプターを取り込むか、チェックボックスで指定
  8. 赤ボタンクリックする
  9. 取り込み画面

これで取り込みが行われます。

読み込みが完了したら、

  1. 「ディスク作成」タブを選択する
  2. 「コンテンツ」タブを選択する
  3. 「追加の動画をインポート」アイコンをクリックし、取り込んだ動画を選択する
  4. 「このタイトルを編集する」アイコンをクリックする
  5. コンテンツ

最後の手順で、動画の編集へ遷移します。
コンテンツ編集

この画面でPowerProducerでも行えた

  • 削除
  • 分割
  • 結合
  • サムネイル選定

をはじめ、その他多数の編集が可能です。

次回は、タイトルメニューについて紹介します。

VHS(またはVHS-C)からBD(またはDVD)作成【2】〜DVDからPCへの取り込み(PowerProducer編)〜

ダビングしたDVDができた後、次はこのDVDからPCへデータの取り込みを行います。

今回はオーサリングソフトを使った取り込み〜編集を紹介します。
お薦めするオーサリングソフトは、

  • PowerProducer
  • PowerDirector

です。どちらもCyberLink社製のオーサリングソフトになります。
PowerProducerは初心者向け、簡単に作成したい人向けです。
PowerDirectorはタイトルメニューを自分で作成したい人には必須です。
自分で作成しない場合は、プリインストールされたテンプレートかCyberLinkのオンラインコミュニティー(DirectorZone.com) で無料のテンプレートをダウンロードして利用することができます。

まずは、PowerProducerでの取り込み〜編集を紹介します。

ダビングしたDVDをPCが認識できる状態で、PowerProducerを起動します。

  1. 「ムービーディスクを作成」を選択する。
    PowerProducerトップ画面
  2. 「DVD」or「ブルーレイディスク」を選択して、「次へ」。
    対象ディスク選択
  3. 「ディスクのシーン」を選択する。
    読み込み/編集
  4. 取り込むタイトルあるいはチャプタを選択し、ディスクから取り出すアイコンをクリックする。(タイトル、チャプタは複数選択可能。)
    読み込み

これで取り込みが行われます。
タイトルをチェックするとタイトルを1つのファイルに。チャプタをチェックするとそのチャプタを1つのファイルに出力します。1つのファイルは3.27GBぐらい(約50分)と決まっているらしく、勝手に分割されます。あとの編集で結合することもできますが、繊細な人だと結合したことが分かるレベルなので、50分以内の適当なところ(シーン転換するようなところなど)でタイトルを分割するかチャプターを打っておくのがベストです。

ちなみに取り込んだ動画は、
【ドライブ名】:\Users\【ユーザ名】\Cyberlink\PowerProducer
配下に.mpgという拡張子で作成されます。

私の場合、これを日付とタイトルからなる名前でリネームして保存することを全てのVHSに対して先に行い、次の編集作業は全てのVHSをPCに保存してから行うようにしました。というのも、沢山あるVHSの年代順の考慮や、1枚のDVD、BDに入る容量などを把握するのに、圧倒的にPC上で行った方が効率が良かったからです。
1度PCに保存すれば、
先の手順で「ディスクのシーン」を選択していたところで、「動画」を選択して、直接保存した動画を読み込めます。
動画ファイルの読み込み

読み込みが完了したら、「ビデオクリップ」を選択します。
ビデオクリップへ
ビデオクリップ
ここで、タイトルの

  • 削除
  • 分割
  • 結合
  • サムネイル選定

といった編集を行います。
VHSのレベルではできなかったフレーム単位での編集ができます。
サムネイルはタイトルメニュー画面で表示されます。デフォルトでタイトル先頭シーンが採用されますが、
VHSから取り込んだ場合、タイトルの先頭にトラッキングノイズが発生していることが多いので、
別のシーンを選定するほうが良いと思います。
ちなみにデフォルトではモーションサムネイルといって、選択したシーンから十数秒を繰り返し再生するようなサムネイルがタイトル画面メニューで表示されます。(赤点の部分で再生されます。)
モーションサムネイル

次はPowerDirectorでの取り込み〜編集を紹介します。

VHS(またはVHS-C)からBD(またはDVD)作成【1】〜VHSからDVDへのダビング〜

このたび、20年以上前のVHS-CやVHSをBDへ書き込む作業を行いましたので、レポートしておきます。
VHSを再生できる機器が次第に無くなってきている昨今、同じようなことを考えておられる方のお役に立てればと思います。

  1. VHSからDVDへのダビング
  2. DVDからPCへの取り込み
  3. BD(あるいはDVD)メニュー作成
  4. BD(あるいはDVD)への書き込み

という順番に説明していきます。

まずは、VHSからDVDへのダビングについてです。
今回の作業にはVHSを再生でき、それをデジタル化できる機器が必要です。
そこで、VHSからDVDへダビングできる機器を探しました。

私が選んだのは、
DXアンテナ製 地上デジタルチューナー内蔵ビデオ一体型DVDレコーダー DXR160V
DXR160V

です。現在も新品で手に入れることができる数少ない製品です。
大事な映像データですので、良い状態でダビングするためにも新品を選びました。
新品で手に入れても、作業後オークションなどに出品すれば、実質数千円程度のコストで済みます。
また、VHSなどからデジタル化を代行してくれる業者は沢山ありますが、
量が多い場合は自分でやった方が断然安くできます。

今回、デジタル化する対象には、VHS-Cもありましたので、VHSへ変換するアダプタも必要でした。
VHS-CはVHSよりもコンパクトなカセットで、アダプタを利用することでVHSとして使うことができます。

このような感じのものです。当時使っていたVictor製のアダプタが残っていたので今回はそれを使います。
VHS-C アダプター

最終的にBDではなく、DVDにするのであれば、ダビングするだけで作業は終わってしまうのですが、
DXR160Vに限らず、一般的なレコーダはPCのDVD編集(タイトルメニューなどの作成)機能と比べて若干使いにくいので、
一旦DVDにダビングし、DVDからPCへ取り込み、PCで編集したほうが良いと思います。
そのため、ダビング先となるDVDはDVD-R(1回だけ書き込み可能)ではなく、DVD-RW(何回でも書き込み可能)を使い、
DXR160VとPC間のデータの運び屋として使うと良いと思います。
つまり、
VHS→DVD-RW(運び屋)→PC(編集して)→DVD or BD
というデータの流れになります。

以後の作業も含め、DXR160Vのリモコン内で使用するボタンは、赤で示したボタンのみで、操作はシンプルです。
DXR-160Vリモコン

まず、ダビングの手順です。

  1. VHS(またはVHS−Cをセットしたアダプタ)とDVDをDXR160Vに入れます。
  2. 「DVD」ボタンでDVDモードに切り替えます。
  3. 「録画モード」ボタンで所望の録画モードを選択します。(私は映像劣化をできるだけ少なくするために、XPにしました。)
  4. 「ビデオ」ボタンでビデオモードに切り替えます。
  5. 「再生」と「停止」ボタンを使い、ダビング開始位置でVHSを停止させます。
  6. 「ダビング」ボタンでダビングを開始します。
  7. ダビング終了位置にて「停止」ボタンでダビングを終了します。

これでダビングができます。
録画モードがXPであれば、1時間程度の映像をDVDへ入れることができます。
その時間範囲で、最後の3手順を繰り返すことができます。

説明が前後しますが、
使用するDVD-R(またはDVD-RW)はフォーマットしておく必要があります。
DXR160Vでも簡単にできます。

  1. DVDをDXR160Vに入れます。
  2. 「DVD」ボタンでDVDモードに切り替えます。
  3. 「セットアップ」ボタンで画面遷移します。
  4. 「ディスク管理」を選択して、「決定」ボタン。
  5. 「フォーマット」→「VRモード」を選択して、「決定」ボタン。

dvdformat
です。VRモードでフォーマットして下さい。

また、DVD-RWを運び屋として使う場合は、作業の前に前回のタイトルを消す必要があります。これもDXR-160Vで簡単にできます。(フォーマットしてしまうのもありだと思います。)

  1. DVDをDXR160Vに入れます。
  2. 「DVD」ボタンでDVDモードに切り替えます。
  3. 「トップメニュー」ボタンで画面遷移します。
  4. 表示されたタイトル一覧で削除するタイトルを選択して、「決定」ボタン。
  5. 「タイトルを削除」を選択すると削除できます。

タイトル削除
で、全タイトルを削除して、再び、1時間分貯める、という作業を繰り返すことになるかと思います。
2枚程度DVD-RWを用意し、ローテーションさせて、次の作業である「DVDからPCへの取り込み」と並行に作業すると良いと思います。
(どちらも時間を要する処理ですので。)

次はその「DVDからPCへの取り込み」を紹介します。

コマンドラインからmp4動画の任意場面をキャプチャする

キャプチャに使用するプログラムはフリーの動画プレイヤーとして有名なVLC Media Playerです。
インストールするとGUIで使用できるのはもちろんですが、CUIでも使用できます。
そして、VLCにはキャプチャ機能が備わっており、CUIからも使用できます。つまり、これはプログラムから起動(例えばfork&exec)させ、裏(GUI表示なし)でキャプチャができることを意味しています。

キャプチャは以下のコマンドで可能です。

cvlc hoge.mp4 --rate=1 --video-filter=scene --vout=dummy --start-time=0 --stop-time=0.5 --scene-format=jpg --scene-ratio=500 --scene-prefix=foo --scene-replace --scene-path=. vlc://quit

start-timeとstop-timeでキャプチャする対象となるシーンの位置を指定します。(単位は秒)
できるだけコマンド実行を早く終わらせるために、start-timeとstop-timeの間は小さくしたほうがいいです。(ちなみに同じ値にするとキャプチャできません。)
scene-ratioはキャプチャをする間隔(何フレーム毎にキャプチャするか)で、上記のように比較的大きな値にしておけばキャプチャは1度しか行われません。(上記の場合0.5秒間なので500フレームもあるはずないということです。最初のフレームをキャプチャした後、次のキャプチャまで500フレーム待ちますが、その前にstop-timeになって終了、、、という流れです。)

実行後、実行したディレクトリに
foo.jpg
というファイルができているはずです。

inotifyによるLinuxファイルシステムイベントの監視

Linuxファイルシステムに発生したイベントを監視するために、inotifyというAPIを使うことができます。
プログラム実行ディレクトリ以下を監視するプログラムのソースコードとその動作結果を掲載します。

まず、ソースコード(inotify_sample.c)です。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dirent.h>
#include <sys/stat.h>
#include <sys/inotify.h>
#include <sys/select.h>
#include <limits.h>

#define WATCH_DIR "."

//イベントサイズは16バイト境界
#define INOTIFY_EVENT_MAX (((sizeof(struct inotify_event)+NAME_MAX+1)+16)&~16)

typedef struct _WD_INFO
{
  struct _WD_INFO* prev;
  struct _WD_INFO* next;
  int wd;
  char* path;
} WD_INFO;

static WD_INFO* topWdInfo = NULL;

static void getWdInfo(int fd, char* dirname)
{
  DIR* dir = NULL;
  struct dirent* entry;
  struct stat st;
  int wd;
  int dirname_len;
  int entname_len;
  char* fullpath = NULL;
  WD_INFO* newWdInfo;

  newWdInfo = (WD_INFO*)malloc(sizeof(WD_INFO));

  if(topWdInfo == NULL){
    // first
    topWdInfo = newWdInfo;
    topWdInfo->prev = topWdInfo;
    topWdInfo->next = topWdInfo;
  }else{
    newWdInfo->prev = topWdInfo->prev;
    topWdInfo->prev->next = newWdInfo;
    topWdInfo->prev = newWdInfo;
    newWdInfo->next = topWdInfo;
  }

  newWdInfo->wd = inotify_add_watch(fd, dirname, IN_ALL_EVENTS);
  newWdInfo->path = strdup(dirname);

  //Search Sub directry
  dir = opendir(dirname);

  dirname_len = strlen(dirname);

  while((entry = readdir(dir)) != NULL){

    if(strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0){
      continue;
    }

    //エントリのタイプ種別を非標準のd_typeを使わず、statで取得
    entname_len = strlen(entry->d_name);
    fullpath = (char*)malloc(dirname_len + 1 + entname_len + 1);
    strcpy(fullpath, dirname);
    strcat(fullpath, "/");
    strcat(fullpath, entry->d_name);
    stat(fullpath, &st);

    if(S_ISDIR(st.st_mode)){
      //再帰呼び出し
      getWdInfo(fd, fullpath);
    }

    free(fullpath);
  }

  closedir(dir);
}


static char* wd2path(int wd)
{
  WD_INFO* p;

  if(topWdInfo == NULL){
    return NULL;
  }

  p = topWdInfo;
  do{
    if(p->wd == wd){
      return p->path;
    }
    p = p->next;
  }while(p != topWdInfo);

  return NULL;
}

static void closeAllWdInfo(int fd)
{
  WD_INFO* p;

  if(topWdInfo == NULL){
    return;
  }

  p = topWdInfo;
  do{
    WD_INFO* del;
    del = p;
    p = p->next;
    free(del->path);
    inotify_rm_watch(fd, del->wd);
    free(del);
  }while(p != topWdInfo);
  topWdInfo = NULL;
}

static void deleteWdInfo(int wd)
{
  WD_INFO* p;

  if(topWdInfo == NULL){
    return;
  }

  p = topWdInfo;
  do{
    if(p->wd == wd){
      if(p->next == p->prev){
        topWdInfo = NULL;
      }else{
        if(p == topWdInfo){
          topWdInfo = p->next;
        }
        p->next->prev = p->prev;
        p->prev->next = p->next;
      }
      free(p->path);
      free(p);

      return;
    }
    p = p->next;
  }while(p != topWdInfo);

  return;
}

int main(int argc, char** argv)
{
  struct timeval waitval;
  int fd;
  int ret;
  fd_set readfds;

  fd = inotify_init();

  getWdInfo(fd, (char*)WATCH_DIR);

  while(1){
    FD_ZERO(&readfds);
    FD_SET(fd, &readfds);
    ret = select(fd+1, &readfds, NULL, NULL, NULL);
    if(0 < ret){
      if(FD_ISSET(fd, &readfds)){
        char* buf;
        int len;
        struct inotify_event* event;

        buf = (char*)malloc(INOTIFY_EVENT_MAX);

        //INOTIFY_EVENT_MAXを指定し、最低でも一つのイベントは読み込む。
        len = read(fd, buf, INOTIFY_EVENT_MAX);

        event = (struct inotify_event*)buf;

        //複数イベントがあるかもしれない。全部処理するまでループ。
        while(len > 0){
          char* target;
          if(event->len){
            target = event->name;
          }else{
            target = wd2path(event->wd); 
          }


          if(event->mask & IN_ACCESS){
            printf("[%s] was accessed.\n", target);
          }
          if(event->mask & IN_MODIFY){
            printf("[%s] was modified.\n", target);
          }
          if(event->mask & IN_ATTRIB){
            printf("Metadata of [%s] changed.\n", target);
          }
          if(event->mask & IN_CLOSE_WRITE){
            printf("Writtable [%s] was closed.\n", target);
          }
          if(event->mask & IN_CLOSE_NOWRITE){
            printf("Unwrittable [%s] closed.\n", target);
          }
          if(event->mask & IN_OPEN){
            printf("[%s] was opened.\n", target);
          }
          if(event->mask & IN_MOVED_FROM){
            printf("[%s] was moved from X.\n", target);
          }
          if(event->mask & IN_MOVED_TO){
            printf("[%s] was moved to Y.\n", target);
          }
          if(event->mask & IN_CREATE){
            printf("[%s] was created in [%s].\n", event->name, wd2path(event->wd));
            char* dirname;
            int dirname_len;
            int eventname_len;
            char* fullpath;
            struct stat st;

            dirname = wd2path(event->wd);
            eventname_len = strlen(event->name);
            fullpath = (char*)malloc(dirname_len + 1 + eventname_len + 1);
            strcpy(fullpath, dirname);
            strcat(fullpath, "/");
            strcat(fullpath, event->name);
            stat(fullpath, &st);

            if(S_ISDIR(st.st_mode)){
              //監視対象追加
              getWdInfo(fd, fullpath);
            }
          }
          if(event->mask & IN_DELETE){
            printf("[%s] was deleted in [%s].\n", event->name, wd2path(event->wd));
          }
          if(event->mask & IN_DELETE_SELF){
            printf("[%s] was deleted.\n", target);
          }
          if(event->mask & IN_MOVE_SELF){
            printf("[%s] was moved.\n", target);
          }

          if(event->mask & IN_IGNORED){
            printf("[%s] was ignored.\n", target);
            //監視対象削除
            deleteWdInfo(event->wd);
          }

          len -= (sizeof(struct inotify_event) + event->len);
          event = (struct inotify_event*)(((char*)event)+sizeof(struct inotify_event) + event->len);
        }
        free(buf);
      }
    }
  }

  closeAllWdInfo(fd);
  close(fd);

  return 0;
}

main()から見ていきます。
まず、inotify_init()にて監視対象の元締めみたいなものを作ります。その後、getWdInfo()を再帰的に呼び出して、カレントディレクトリ以下のディレクトリ全てを、inotify_add_watch()を使って、元締めに対して登録していきます。後はselect()を使って、元締めを監視します。監視対象のディレクトリ内でなんらかのイベントが発生すると、それを元締めが

/* Structure describing an inotify event.  */
struct inotify_event
{
  int wd;		/* Watch descriptor.  */
  uint32_t mask;	/* Watch mask.  */
  uint32_t cookie;	/* Cookie to synchronize two events.  */
  uint32_t len;		/* Length (including NULs) of name.  */
  char name __flexarr;	/* Name.  */
};

この構造体を通じて詳細に教えてくれます。教えてもらうには元締めからread()します。
構造体に記載の__flexarrは[0]のことであり、通知イベントが可変長であることを示しています。
read()時に気をつけなければならないのは、必ず一つのイベントを読みきるということです。read()時に指定するサイズが小さいとエラーを返します。
このことは構造体のwdからlenまでをまず読んで、lenにしたがって後のデータを読む、という誰もが考える効率の良い読み方ができないことを示しています。私はこれにハマってしまいました。気をつけてください。
とういうことで、read()に指定するサイズは考えられる最大イベントサイズを指定する必要があり、nameのMAXがlimits.hで定義されているNAME_MAX、かつ(実際実行してみたところ、)必ず16の倍数サイズにされるようなので、

#define INOTIFY_EVENT_MAX (((sizeof(struct inotify_event)+NAME_MAX+1)+16)&~16)

のようにread()サイズを指定しました。

あとはイベント内容を読み取り、表示しています。また、ディレクトリの追加イベント((IN_CREATE)発生時は、監視対象が増えたので、元締めに対してinotify_add_watch()を行ってます。監視対象削除イベント(IN_IGNORED)に対しては、保持している内部データWD_INFOから対象を削除しています。監視対象削除イベント発生時に自動的にinotify_rm_watch()相当が行われるため、inotify_rm_watch()は行わないで良いようです。

コンパイルしてみました。

$ gcc -g inotify_sample.c -o inotify_sample.c

動作を見るためにプログラム実行ディレクトリを、以下のような環境にしておきます。

$ find . -print
.
./foo
./foo/bar
./foo/bar/bar0
./inotify_sample.c
./inotify_sample
./hoge
./hoge/hoge0

環境を整え、実行してみました。

$ ./inotify_sample
[.] was opened.
[foo] was opened.
[./foo] was opened.
[bar] was opened.
[./foo/bar] was opened.
Unwrittable [bar] closed.
Unwrittable [./foo/bar] closed.
Unwrittable [foo] closed.
Unwrittable [./foo] closed.
[hoge] was opened.
[./hoge] was opened.
Unwrittable [hoge] closed.
Unwrittable [./hoge] closed.
Unwrittable [.] closed.

実行するとパラパラとイベントが発生しているのがわかります。
これらはinotify_sampleプログラムで監視対象を登録するためにopendir()等を行っているために発生しています。つまり、自分自身がイベントを発生させているわけです。

このまま、ファイルシステムに変更を与えた時の動作を見ていきました。
(結果を見ると動作は理解できると思いますので、説明は特にいれません。)
プログラム実行しているターミナルとは別ターミナルから

$ touch hoge/hoge1

とすると、プログラム実行側のターミナルでは、

[hoge1] was created in [./hoge].
[hoge1] was opened.
Metadata of [hoge1] changed.
Writtable [hoge1] was closed.

のようになります。その他、いろいろ操作してみました。

————————————————–

$ rm hoge/hoge1

とすると、

[hoge1] was deleted in [./hoge].

————————————————–

$mkdir foo/bar/fuga

とすると、

[fuga] was created in [./foo/bar].
[fuga] was opened.
[./foo/bar/fuga] was opened.
Unwrittable [fuga] closed.
Unwrittable [./foo/bar/fuga] closed.

————————————————–

$ mv foo piyo

とすると、

[foo] was moved from X.
[piyo] was moved to Y.
[./foo] was moved.

————————————————–

$ rm -rf piyo

とすると、

[piyo] was opened.
[./foo] was opened.
[bar] was opened.
[./foo/bar] was opened.
[bar0] was deleted in [./foo/bar].
[fuga] was opened.
[./foo/bar/fuga] was opened.
Unwrittable [fuga] closed.
Unwrittable [./foo/bar/fuga] closed.
[fuga] was deleted in [./foo/bar].
Unwrittable [bar] closed.
[./foo/bar/fuga] was deleted.
Unwrittable [./foo/bar] closed.
[bar] was deleted in [./foo].
[piyo] was deleted in [.].
Unwrittable [piyo] closed.
Unwrittable [./foo] closed.
[./foo/bar/fuga] was ignored.
[./foo/bar] was deleted.
[./foo/bar] was ignored.
[./foo] was deleted.
[./foo] was ignored.

————————————————–

正しく動作していそうです。

extファイルシステムにおけるinode番号の無効値

ext2/3/4といったファイルシステムにあるinode番号について。inodeはファイルシステム内にあるファイルやディレクトリなどのオブジェクトを示すものであり、inode番号はそれらを識別するための番号であり、同じファイルシステム上でユニークな値、かつ、正の整数です。上限値は環境に依存しますが、LinuxのC言語ではino_tという型が用意されており、符号無し32ビット整数だったり符号無し64ビット整数だったりします。
このinode番号をプログラムで使用する場合、当然ino_t型の変数を用意して使用することになりますが、初期値や無効値には何を指定すればよいか悩みました。符号無しなので-1を使用することはできません。
いろいろ調べていて、0にするのが適切であるとの見解に至りました

/usr/include/linux/ext2_fs.h(あるいはext3_fs.h)を見てみるとinode番号に関して次のような定義があります。

 

/*
 * Special inode numbers
 */
#define	EXT2_BAD_INO		 1	/* Bad blocks inode */
#define EXT2_ROOT_INO		 2	/* Root inode */
#define EXT2_BOOT_LOADER_INO	 5	/* Boot loader inode */
#define EXT2_UNDEL_DIR_INO	 6	/* Undelete directory inode */

/* First non-reserved inode for old ext2 filesystems */
#define EXT2_GOOD_OLD_FIRST_INO	11

10までが予約されていて、通常は11以降の番号が振られる、ということを示しています。0については「Special inode numbers」でもないので、初期値や無効値として利用するにはちょうど良い、と考えました。手元のLinuxで0番が使われていないことを確認してみました。

 

> find / -inum 0 -print
>

使われていませんでした。 ということで、0をinode番号の初期値や無効値として利用することにしました。