2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

格安WiFiカメラ(miSafes D304)で /dev/mem からフレームを抜いてブラウザ実時間視聴する

2
Posted at

格安WiFiカメラ(miSafes D304)で /dev/mem からフレームを抜いてブラウザ実時間視聴する

クラウド(P2P)経由の専用アプリでしか見られない格安WiFiカメラを、
ローカルLANのブラウザでリアルタイム視聴できるようにした記録です。

  • カメラのISPが吐いた生フレームは 物理メモリ上に存在し、/dev/mem から読める(本機は物理 0x4e9ae000 に 1920×1088 YUV420)。
  • 生データをSSHで吸い出してPCで変換…は 転送が遅すぎて実時間にならない(単コア+ISP割り込みで ~30KB/s)。
  • 鍵は 「カメラの中で先にJPEG圧縮してから配る」。圧縮を挟むだけで転送量が激減し、実時間になる。
  • 圧縮は カメラ内でJava(ImageIO) が担当。/dev/mem を読んで小さいグレースケールJPEGにして吐く。
  • 配信は Caddy、表示は ブラウザが live.jpg を連続取得するだけ。
  • システムファイルは一切改変せず/dev/mem を読むだけ+SDから自作バイナリを実行、/opt の純正はそのまま)。
  • 単コアでも 480×270 グレースケールで 3.5〜4 fps 出る。

自分で所有する機器での学習・改造記録です。/dev/mem は強力なので自己責任で。


背景:なぜ自分でやることにしたのか

この手の格安WiFiカメラは、専用スマホアプリ+P2Pクラウド(TUTK系)で初期設定も視聴もする前提です。ところが手元の個体は…

  • サービス/アプリがサポート終了していて、初期設定サーバもP2Pも生きていない。つまり普通にセットアップすることすらできない。本来の使い方が完全に詰んでいて、このままだとただの文鎮(e-waste)。
  • そもそも動いていた頃も、自宅の室内映像が海外(中国)のクラウドサーバを経由する作り。素性のよく分からない海外サーバに家の中の映像を預けるのは、正直かなり気持ち悪い。

要するに 「使えない上に、(使えても)データを外に送られるのが嫌」
でも分解して中を見ると、中身は普通のLinux+普通のISP。だったら答えは一つで、

クラウドを完全に排除して、ローカルLANだけで完結する“自分のカメラ”にしてしまえばいい。

これがこの一連の作業の動機です。方針はシンプルで、映像を一切インターネットに出さないこと。視聴もローカル、録画もローカル。
本記事はその「実時間視聴」の部分を扱い、後半では「ローカルにループ録画させる」ためのファームウェア解析も載せます。


デバイスと制約

項目 内容
SoC InfoTM iMAPx15(単コア ARM Cortex‑A5)
RAM 約 200MB / rootfs は read‑only
センサ AR0330(MIPI) → ISP(Felix) → H.265/JPEGエンコード+P2P を avserver が一括処理
フレーム ISPの出力が物理メモリに存在。/dev/mem から読める
ネット SSHのスループットが遅い(ISPのIRQ/DMAでCPU飽和時 ~30KB/s)

全体像


1. フレームはどこにある?

avserver がセンサ→ISP→エンコードのパイプラインを回しています。
ISP(Felix)の出力フレームは物理メモリ上のバッファに置かれ、/dev/mem 経由でその物理アドレスを読めることが分かりました。

  • 物理アドレス: 0x4e9ae000
  • サイズ: 3133440 バイト = 1920 × 1088 の YUV420(Y=1920×1088=2088960B + UV)

グレースケールにした理由:この機種のUV(色差)プレーンはタイル状(mosaic)に並んでいて素直に扱えません
一方 Y(輝度)プレーンはそのまま画像になるので、Yだけ使う=グレースケールが一番手堅い。
(監視用途なら白黒で十分実用的)

/dev/mem を読むだけなので、カメラの純正プロセスやシステムファイルには一切触りません


2. 素朴な方法の限界(そして転機)

最初はこうしました:

# SSH越しに /dev/mem を dd で吸い出して…
ssh root@cam "dd if=/dev/mem bs=4096 skip=... count=510" > frame.y
# …PC側(Python+Pillow)でJPEG化

これが 遅すぎる。生のYプレーンだけで約2MB、SSHのスループットが遅く(単コア+ISP割り込みでCPUが埋まる)、1フレームに数秒。実時間には程遠い。

ここでの気づきが全てでした:

「取り出したフレームを、カメラの中で別プログラムで圧縮してから配ればいいのでは?」

生データをネットワークに流すのをやめ、カメラ内で先にJPEG(数十KB)にする。転送量が1/50以下になり、一気に実時間になります。


3. カメラ内でJPEG圧縮(Java + ImageIO)

圧縮役は カメラ内で動かしたJava(ARM JRE8)
uClibc環境でglibcリンクのJavaを動かす方法(glibc同梱+ELF interpを/tmp/ldにパッチ)は
Minecraftサーバー編 と同じ手法です。

Live.java(要点):

import java.io.*; import java.awt.image.*; import javax.imageio.*; import javax.imageio.stream.*;

public class Live {
  public static void main(String[] a) throws Exception {
    long phys = Long.decode(a[0]);          // 0x4e9ae000
    int W=1920, H=1088;                      // 元フレーム
    int OW=Integer.parseInt(a[1]), OH=Integer.parseInt(a[2]); // 出力 例 480x270
    float q = Integer.parseInt(a[3]) / 100f; // JPEG品質
    String out = a[4];                       // /mnt/mmc/live.jpg

    byte[] row = new byte[W];
    BufferedImage small = new BufferedImage(OW, OH, BufferedImage.TYPE_BYTE_GRAY);
    byte[] sb = ((DataBufferByte) small.getRaster().getDataBuffer()).getData();
    int[] cx = new int[OW];
    for (int ox=0; ox<OW; ox++) cx[ox] = ox * W / OW;   // 列の間引き先を先に計算

    ImageWriter w = ImageIO.getImageWritersByFormatName("jpeg").next();
    ImageWriteParam p = w.getDefaultWriteParam();
    p.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
    p.setCompressionQuality(q);

    RandomAccessFile mem = new RandomAccessFile("/dev/mem", "r");
    while (true) {
      // ★ストライド読み: 全2MBを読まず、出力に必要な行だけ seek+read
      for (int oy=0; oy<OH; oy++) {
        mem.seek(phys + (long)((oy * H / OH)) * W);
        mem.readFully(row, 0, W);
        int orow = oy * OW;
        for (int ox=0; ox<OW; ox++) sb[orow + ox] = row[cx[ox]]; // 手動ニアレストネイバー
      }
      // アトミック更新: tmpに書いてrename(配信側が半端なファイルを掴まない)
      File ft = new File(out + ".tmp");
      ImageOutputStream ios = new FileImageOutputStream(ft);
      w.setOutput(ios);
      w.write(null, new IIOImage(small, null, null), p);
      ios.close();
      ft.renameTo(new File(out));
    }
  }
}

fps最適化の肝

  • drawImage で縮小せず、必要な行だけ /dev/mem から読む(ストライド読み)手動で列を間引く
  • 全フレーム(2MB)を読むのをやめ、出力解像度(例 480×270)ぶんの行だけ触ることで 1.58fps → 3.5〜4fps に。
  • 出力をいったん .tmp に書いて rename することで、配信側が書きかけJPEGを掴まない

4. Caddyで配信 → ブラウザでポーリング

配信は ファイルサーバー編 と同じ Caddy(Go静的バイナリ)で /mnt/mmc を配るだけ。

XDG_CONFIG_HOME=/tmp/caddy XDG_DATA_HOME=/tmp/caddy \
  /mnt/mmc/caddy file-server --root /mnt/mmc --listen :8080 </dev/null > /mnt/mmc/caddy.log 2>&1 &

表示ページ index.htmllive.jpg を連続取得してfps表示):

<!doctype html><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1">
<style>body{background:#111;margin:0;text-align:center;color:#bbb;font-family:sans-serif}
img{max-width:100%;max-height:90vh}#s{font-size:.85em;padding:4px}</style>
<div id=s>connecting...</div><img id=i>
<script>
let n=0, t=Date.now();
function load(){
  let x=new Image();
  x.onload=function(){                       // 読めた瞬間に表示 → 次を取りに行く
    document.getElementById('i').src=x.src; n++;
    let fps=(n/((Date.now()-t)/1000)).toFixed(2);
    document.getElementById('s').textContent='LIVE  frame '+n+'  ~'+fps+' fps';
    setTimeout(load,120);
  };
  x.onerror=function(){setTimeout(load,600);};
  x.src='/live.jpg?'+Date.now();             // キャッシュ回避のタイムスタンプ
}
load();
</script>

ポイントは img.onload チェーン。1枚読めたら即座に次を要求するので、
サーバ更新レートに自然に追従し、無駄なリクエストも出ません。あとは http://<IP>:8080/ を開くだけ。


5. 起動の組み立て(システムを触らない配慮)

testlauncher.sh のカメラモードでの流れ(要点):

start_camera() {
    cp /mnt/mmc/gl2/ld-linux-armhf.so.3 /tmp/ld; chmod +x /tmp/ld   # Java用glibcローダ

    # 1) 純正 avserver を起動 → ISPがフレームを /dev/mem に生成 (バイナリは無改変)
    /opt/ipnc/avserver_q3.bin </dev/null >/tmp/av.log 2>&1 &

    # 2) SSHを最優先・重い処理を低優先度に (カメラ稼働中もSSHで入れるように)
    renice -20 -p "$(cat /tmp/dropbear.pid)"      # SSH最優先
    renice  15 -p "$(cat /tmp/av.pid)"            # avserver下げる

    # 3) Caddy配信 → ISP起動待ち → Java圧縮ループ
    "$CADDY" file-server --root /mnt/mmc --listen :8080 </dev/null >/mnt/mmc/caddy.log 2>&1 &
    sleep 18
    LD_LIBRARY_PATH="$GL2:$JRE/lib/aarch32:$JRE/lib/aarch32/jli:$JRE/lib/aarch32/server" \
        "$JRE/bin/java" -Xmx48M -Djava.awt.headless=true -cp /mnt/mmc \
        Live 0x4e9ae000 480 270 60 /mnt/mmc/live.jpg 0 </dev/null >/tmp/live.log 2>&1 &
    renice 10 -p "$(cat /tmp/live.pid)"
}
  • /opt/ipnc の純正バイナリはそのまま使い、フレーム生成だけ担わせる。読むのは /dev/mem
  • 自作物(Live.class, interpパッチ済みjava)はSDから実行。書き換えが要る箇所はtmpfsに逃がす。
  • OSDのタイムスタンプがズレる問題は、起動時に NTP同期して解決(busybox ntpd -nq -p pool.ntp.org)。

6. ローカルにループ録画させる(ファームウェア解析と改変)

視聴だけでなく、クラウド無しでSDにループ録画もさせたい。純正はアプリ/クラウドの制御下でしか録画しないので、録画パイプラインの本体 avserver を解析しました。

幸運:シンボルが残っていた

avserver_q3.bin はセンサ/ISP/エンコード/P2Pまで全部入りの一枚岩バイナリ。しかも ストリップされておらず約13,000個のシンボルが残っていました。関数名が見えるのでリバースエンジニアリングが一気に楽に。capstone(Python)でThumbコードを逆アセンブルして追っていきます。

コマンド・ディスパッチを特定

内部のIPC処理に、msgid(0xC9系)で分岐する tbh(テーブル分岐ハーフワード)ジャンプテーブル を発見。ケースはおよそ 201〜215。中身を読むと役割が見えてきます:

msgid 意味
202 ナイトモード切替
203 SDカード挿入イベント → ここが録画開始のトリガ
206 日時更新

変えた部分(バイナリパッチ)

録画をクラウド/アプリの許可に関係なく回すため、バイナリを直接パッチしました:

  • 録画パスをスキップしている cbz(ゼロ比較分岐)ゲートを nop で潰す
  • ジャンプテーブルを張り替えて、録画側のハンドラへ確実に通す

パッチ済みバイナリは SDカードから実行し、/opt/ipnc の純正 avserver_q3.binそのまま残す。走らせなければ完全に元通り=リバーシブルです。

正直な失敗談

録画ファイルを短く区切りたくて、0x1b3f6 にあった movw #0x259(=601) を「これがセグメント長だろう」と当て推量で 20 に書き換えたら…それは長さではありませんでした。avserverが暴走してCPUを食い尽くし、SSHのバナー交換すら通らなくなり、電源再投入以外で復帰不能に。

教訓:確証のない値を勘でパッチしない。相互参照や実挙動で意味を確定してからビットを触ること。

なお最終的な「実時間視聴」は純正avserverが吐いたフレームを /dev/mem から読むだけなので、この録画パッチ自体は必須ではありません。ただ、この解析で**バイナリの“地図”**ができたからこそ、フレームがメモリのどこに出るか当たりを付けて /dev/mem 作戦にたどり着けた——という順番でした。


7. ハマりどころ

症状 原因 対策
PCで変換すると数秒/frame 生データ(2MB)のSSH転送が遅い カメラ内で先にJPEG圧縮してから配る
色がぐちゃぐちゃ UVプレーンがタイル状(mosaic) Y(輝度)だけ使う=グレースケール
fpsが上がらない 毎フレーム全2MB読んでいた ストライド読み+手動間引き(必要な行だけ)
配信画像が時々割れる 書きかけJPEGを配信 tmp+renameでアトミック更新
カメラ稼働中にSSHが固まる 単コア+ISP割り込みでCPU飽和 dropbearをrenice -20/最悪は電源再投入
OSDの時刻が2016年 時計未同期 起動時に NTP同期
Javaが起動しない uClibcにglibcローダ無し glibc同梱+interpを/tmp/ldにパッチ(別記事参照)
avserver暴走でSSHロック セグメント長と誤認して勘でパッチ 確証のない値は触らない/復帰は電源再投入

8. まとめ

  • ISPのフレームは物理メモリに居て /dev/mem から読める。ここが入口。

  • 実時間化の決め手は 「ネットに生データを流さない、カメラ内で先に圧縮する」。たったこれだけで世界が変わる。

  • 圧縮は手近な Java(ImageIO) で十分。ストライド読み+間引きで単コアでも 3.5〜4fps。

  • 配信は Caddy、表示は ブラウザの img.onload チェーン。どちらも拍子抜けするほど簡単。

  • そして全部 システムファイル無改変・リバーシブルで実現できる。

  • そして視聴も録画も、映像はLANの外に一切出さない。動機だった「クラウド送信が嫌」も同時に解決。

サポート終了で文鎮化しかけていた格安カメラが、**クラウドを完全に排除した“自分のローカルカメラ”**として復活しました。
「中身はLinux+普通のISP」なので、/dev/mem という一点さえ掴めば意外と何とかなります。
海外クラウド前提のIoT機器を、手元で飼い慣らす一例になれば幸いです。

環境: miSafes D304 / InfoTM iMAPx15 (Cortex‑A5, uClibc, RAM~200MB, Linux 3.10) / ARM JRE8 + Caddy

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?