2011年12月15日
Android 4.0のスクリーンキャプチャ
スクリーンキャプチャについて調べました。KZM-A9-DualにポーティングしたAndroid4.0でうまく動かなかった部分があったのでソースを修正してみました。
生のフレームバッファをのぞく
Androidに限らず、ポーティングをしていて特にエラーになっていないのにもかかわらず画面が真っ黒のままという場合、フレームバッファデバイスを直接読み出してファイルに書き出し、それをグラフィックソフトでRaw画像として開いてみてみれば、切り分けすることができます。フレームバッファには何か描いてあるのであれば、フレームバッファのデバイスドライバの問題だし、そうでなければユーザーランド側の問題です。
# cat /dev/graphics/fb0 > /data/local/tmp/img.raw
このimg.rawをグラフィックソフト、例えばGimpで開きます。offset, width, heightも指定する必要があります。この場合ならoffset=0。widthはスクリーンの幅。(KZM-A9-Dualならば800)。heightはimg.rawのファイルのサイズ / 4(bytes per piexel) / width を指定します。
screencapコマンド
Android 4.0以前ではスクリーンショットを撮るためにはUSBケーブルで接続してDDMSの機能を使う必要がありました。Android4.0ではscreencapコマンドでデバイス単体で簡単にスクリーンショットを撮ることができるようになりました。
$ screencap -h usage: screencap [-hp] [FILENAME] -h: this message -p: save the file as a png. If FILENAME ends with .png it will be saved as a png. If FILENAME is not given, the results will be printed to stdout. $
指定したファイル名の末尾が.pngの場合はpngフォーマットで、そうでなければ、width, height, formatの各4バイト、計12バイトのヘッダをつけてその後にフレームバッファの生データを出力します。
DDMSはスクリーンショットを撮るときにデバイス側のadbdと通信するのですが、Android2.3まではadbdが直接 /dev/gaphics/fb0 をオープンして中を読んでいましたが、Android4.0ではadbdからscreencapコマンドをfork&execするようになりました。
system/core/adbd/framebuffer_service.c
screencapのバグ
KZM-A9-DualボードにポーティングしたAndroid4.0でスクリーンショットを撮ると、赤と青の色が逆になった画像になってしまいました。screencapコマンドでpngファイルを生成する場合とDDMSを使う場合の両方を試しましたが、どちらもそうなりました。
KZM-A9-DualのフレームバッファのフォーマットはBGRA_8888ですが、screencapコマンドのソースを見ると、32bits per pixelのフレームは常にRGBA_8888として扱うようになっていました。そのために赤と青がひっくり返っていました。
PARTNER-Jetでブレークポイントを仕掛けてみて、原因の部分が特定できたので簡単に修正してみました。以下はそのパッチ。
frameworks/base
diff --git a/cmds/screencap/screencap.cpp b/cmds/screencap/screencap.cpp index 7a599e9..46c7386 100644 --- a/cmds/screencap/screencap.cpp +++ b/cmds/screencap/screencap.cpp @@ -73,8 +73,11 @@ static status_t vinfoToPixelFormat(const fb_var_screeninfo& vinfo, *bytespp = 3; break; case 32: - // TODO: do better decoding of vinfo here - *f = PIXEL_FORMAT_RGBX_8888; + if (vinfo.red.offset == 16) { // ARGB8888 + *f = PIXEL_FORMAT_BGRA_8888; + } else { + *f = PIXEL_FORMAT_RGBX_8888; + } *bytespp = 4; break; default: @@ -83,6 +86,30 @@ static status_t vinfoToPixelFormat(const fb_var_screeninfo& vinfo, return NO_ERROR; } +char const* +exchange_rb(char const* base, size_t size) +{ + char* mapbase; + char r, g, b, a; + int i; + + mapbase = (char*)mmap(0, size, PROT_READ|PROT_WRITE, MAP_ANONYMOUS|MAP_PRIVATE, -1, 0); + if (mapbase != MAP_FAILED) { + while (i < size) { + b = base[i]; + g = base[i + 1]; + r = base[i + 2]; + a = base[i + 3]; + mapbase[i] = r; + mapbase[i + 1] = g; + mapbase[i + 2] = b; + mapbase[i + 3] = a; + i += 4; + } + } + return mapbase; +} + int main(int argc, char** argv) { const char* pname = argv[0]; @@ -163,6 +190,10 @@ int main(int argc, char** argv) if (base) { if (png) { SkBitmap b; + if (f == PIXEL_FORMAT_BGRA_8888) { + base = (void const*)exchange_rb((char const*)base, size); + f = PIXEL_FORMAT_RGBX_8888; + } b.setConfig(flinger2skia(f), w, h); b.setPixels((void*)base); SkDynamicMemoryWStream stream;
pngファイルを作るときにはskiaのライブラリを使っていますが、このライブラリはBGRA_8888のフォーマットをサポートしていないようだったので、イメージをコピーしてRとBをひっくり返すようにしました。
screencapコマンドでpngファイルを生成する場合とDDMSを使う場合の両方でうまくいくことを確認しました。
でも本当は、フレームバッファのフォーマットはデバイス依存なので、HALのモジュールにこの機能を移すほうが正しいやりかたです。フレームバッファに書き込むほうはHALのgrallocを通すようになっているわけですから。
おまけ
Android4.0のソースを"screenshot"で検索すると、TakeScreenshotServiceというServiceを見つけました。
frameworks/base/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java
そのServiceはPhoneWindowManager.javaで使われています。
frameworks/base/policy/src/com/android/internal/policy/impl/PhoneWindowManager.java
VolumeDownキーとPowerキーが同時に長押しされて、なおかつVolumeUpキーが押されていないときにこのServiceが開始されるようです。
Galaxy Nexusではこの操作でスクリーンショットが撮れたそうです。
おまけ2
screencapコマンド以外にもうひとつscreenshotコマンドもあります。
$ screenshot -h unknown option -- husage: screenshot [-s soundfile] filename.png -s: play a sound effect to signal success -i: autoincrement to avoid overwriting filename.png $
こちらは同時に鳴らす音声ファイルを指定することができます。
こちらもBGRA_8888のフォーマットをサポートしていません。