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のフォーマットをサポートしていません。