上の記事でも紹介しましたが、ATOMCamシリーズ、そのまま使うには玄人にはちょっと不便な部分をハックするにあたって、T31のSDKを使って直接新しいアプリケーションを書き、画像を取得することにすでに成功しています。
しかし、今はまだアトムテックのサーバーは生きていますし、同じような機能を実装するのは大変です。
そのため、上の記事ではあきらめていた、既存のアプリに手を入れて、欲しいストリームを横取りすることができないかを調べてみました。
まずは、ダンプから抽出した実行ファイルを眺めてみます。
/sysetem/bin 以下がこんな感じ
そして、/system/libがこんなかんじ
この中からアプリ「iCamera_app」と、そのライブラリ、「localsdk.so」を、定番の「Ghidra」で解析、逆コンパイルして、映像の初期化あたりから辿っていきます。
どうやらこの辺がビデオストリームを生成しているところのようです。
local_sdk_video_createなどの関数が見えてきます。
local_sdk_video_set_encode_frame_callbackという関数があります。
おやおや、もしかすると、これはフレームの到着のたびに呼ばれる仕組みがあるのでは???という期待が出てきました。
localsdk.soを見ていきます。
この関数を調べていくと、get_venc_sb_pstという関数で取得した関数テーブルに外部関数を登録していることがわかりました。
そして、実際にビデオストリームが開始すると、
登録した関数にフレームバッファを渡して呼び出してくれそうなことがわかりました。
(わかりました、って書いてるけど、全然簡単じゃなくて何時間もかかってるのよw)
では、このコールバック登録を横取りして自作の関数を呼び出させてしまえばいいじゃん、ということになります。
しかし、そんなことできるのでしょうか。
Linuxには LD_PRELOAD という呼び出し方法で、実行ファイルから呼び出される外部ライブラリの対象に自作の関数を割り込ませる方法があります。
この方法を使って、コールバック登録関数を上書きして動作を変更します。
本来、コールバック登録関数に渡されるのはATOM Camの中の関数ですが、
その登録作業を横取りし、自作の関数を呼び出させます。そして、そのあと、本来のATOM Camの関数が呼び出されるようにすれば、ATOM Cam側では何も気づかず今までと同じ動作を続けます。
このために、以下のような関数を定義します。
static uint32_t (*real_local_sdk_video_set_encode_frame_callback)(uint32_t param1,uint32_t param2)=NULL; | |
typedef uint32_t (* framecb)(uint32_t); | |
void *pfunccb=NULL; | |
int cnt=0; | |
char fname[255]; | |
static uint32_t test_capture(void *param){ | |
// param is malloc'd pointer | |
if( cnt < 1000) { | |
fprintf(stderr,"callback!!!!!![%p]\n",param); | |
uint32_t *ptr=(uint32_t *)param; | |
uint32_t length=ptr[1]; | |
fprintf(stderr,"pack size=%d\n",length); | |
sprintf(fname,"/media/mmc/stream",cnt); | |
FILE *fp=fopen(fname,"wb"); | |
fseek(fp,0,SEEK_END); | |
fwrite((void *)(*(uint32_t*)param),1,length,fp); | |
fclose(fp); | |
cnt++; | |
} | |
return ((framecb)pfunccb)((uint32_t)param); | |
} | |
uint32_t local_sdk_video_set_encode_frame_callback(uint32_t param1,uint32_t param2){ | |
void *handle; | |
fprintf(stderr,"!!! injected local_sdk_video_set_encode_frame_callback !!!\n"); | |
fflush(stderr); | |
if (real_local_sdk_video_set_encode_frame_callback == NULL){ | |
handle = dlopen ("/system/lib/liblocalsdk.so", RTLD_LAZY); | |
if (!handle) { | |
fputs (dlerror(), stderr); | |
} | |
real_local_sdk_video_set_encode_frame_callback = dlsym(handle, "local_sdk_video_set_encode_frame_callback"); | |
fprintf(stderr,"ptr=%d injected!!! err=%s\n",real_local_sdk_video_set_encode_frame_callback,dlerror()); | |
} | |
fprintf(stderr,"param1=0x%x,param2=0x%x,*param2=0x%x",param1,param2,*(int32_t*)param2); | |
if(param1==0){ | |
pfunccb=param2; | |
fprintf(stderr,"ch0 func injection save pcb=0x%x\n",pfunccb); | |
param2=(uint32_t)test_capture; | |
fprintf(stderr,"override to 0x%x\n",param2); | |
} | |
int ret=real_local_sdk_video_set_encode_frame_callback(param1,param2); | |
return ret; | |
} |
このファイルをコンパイルして、.soファイルを作成します。
mips-linux-uclibc-gnu-gcc -fPIC -shared -o libcallback.so snooper.c -ldl
そして、本来のアプリが呼び出される部分を変更し、
LD_PRELOAD=./libcallback.so.bin /system/bin/iCamera_app &
として呼び出すと、アプリでliblocalsdk内の”local_sdk_video_set_encode_frame_callback”を読んだつもりが、新しく私の書いた関数が呼び出されてしまうのです。
新しく書いたコールバック関数内では、渡されたフレームバッファを単純にファイルに書き出してみました。
うまくいけば、これでカメラから出力され、H264エンコードされたストリームが取得できるはずです。
出力されたストリームを、ffplayに渡してみると
…
やった!!!!画像が出てきました。
どうやらうまくいったようです。
この方法だと、前の記事に書いたように一から新しいアプリを書く必要もなく、もともとのサーバ中継システムを生かしたまま、新たにRTSP配信など、直接データを取り出すことが可能になります。
ここで取り出されたストリームは H264 NAL ストリームなどと呼ばれているようです
ffplayに渡して再生できるので、たぶんffmpegなどに渡してトランスコードするだけで配信サービスができる気がするんですが…ffmpegのコンパイルがちょっとめんどくさそうですね…
とはいえ、前回の記事よりは圧倒的に進歩しました。
せっかくここまではできたので、このままローカルでストリームが取得できるところまでどうにか作ってみたいと思います。