以前、spacedeskを使いWindowsの画面をiPadに映す方法を紹介しました。
ただspacedeskは商用利用の場合料金が発生します。非商用の場合は無料ですが、なんとなくspacedeskを使わずにiPadをWindowsのサブディスプレイとして使えないかなと考えました。
有料であるメリットはたくさんあり、フリーウェア全盛期を知る世代としては無料が常識となると廃れていくこともあります。
ならば無料かつソフトウェア開発が終了しにくい方法でiPadをサブディスプレイ化したらいいのではないいか?Windowsへ専用ソフトを一切入れず、WebブラウザだけでiPadをサブディスプレイにしようと思い、がんばりました。
結果、完成まで3日。3連休が全部潰れるとは思いませんでした。Safariの仕様、FFmpegの挙動、Linuxのプロセスを一つずつ検証した結果、さまざまなものをそぎ落とすことに成功しました。
FFmpeg、v4l-utils、Python標準ライブラリ、この構成まで追い込むことができました。記事の内容としては「こうしたらできました」ではなく「どうしてこの構成」について見てもらえればと思います。
結論
Windowsへ専用ソフトをインストールせず、Raspberry Pi Zero WHを介してiPadをブラウザだけでサブディスプレイとして利用することができました。
ただし、本記事のサブディスプレイはGPUで行うデュアルディスプレイ等とは全く異なります。
Windowsの画面をキャプチャボードで取得して、Raspberry Pi Zero WH上で静止画へ変換し、HTTP経由でSafariへ配信することで、疑似的なサブディスプレイとして画面を表示しています。
きついことと便利なことはあります。
動画を見る、ゲーム、リアルタイムの操作。これらについては論外のレベルです。一方でドキュメントの閲覧、ターミナルを置いておく、Discord、Teams、システム監視、などいわゆる静的な情報については実用十分な性能を発揮できました。
Windows複製モード

Windows標準の画面の複製を利用して、その映像をiPadへ表示しました。
光が足らず画像がが荒くて申し訳ないのですが、一応同じ画面が映っています。遅延はかなりありますが、PDFやWordなどの資料閲覧や軽いプレゼンテーション用途には十分使えます。
Windows拡張モード

サブディスプレイでやりたいことと言えばディスプレイの拡張だと思います。
画像を見るとわかるように画面を横断してPowerShellの画面もマウスカーソルも移動していることがわかります。
Windowsではマルチディスプレイとして扱われ、ブラウザ側でその映像だけを表示しているだけです。
やはりRaspberry Pi Zero WHの性能ではカクツキはどうしようもなく、画面の監視やターミナルを常時表示するなど使い方では結構便利でした。
iPhoneでも拡張モードで表示できる

今回の方法ではsafariを想定して表示するようにしているため、拡張モードで表示できるのはiPadだけではありません。同じURLへアクセスすることで、このようにiPhoneでも表示できます。
つまり、画面を表示する端末はApple Deviceに限らずHTTPへアクセスできるブラウザであれば応用できます。ただし、今回はsafariをターゲットに組みましたので、FirefoxやDuckDuckGoではあまり良い結果は得られませんでした。
この記事でわかること
この記事では、iPadをWindowsのサブディスプレイとして再現する方法だけではなく、設計に関して当初のものと全くことなるものになったことから、個人的におもしろかったため、それらの情報もお伝えします。
具体的には次の内容を書きます。
- Windowsへ専用ソフトをインストールしない理由
- 非力なRaspberry Pi Zero WHでも動作する理由
- FFmpegだけでCPU負荷を削減できた方法
- Python標準ライブラリだけでHTTPサーバーを構築する方法
- Safari特有の問題とその解決方法
- systemdによる常駐化の便利さ
- コマンドとHTMLとJavaScript例
- 検証中の失敗例
- 現在の構成に落ち着いたいきさつ
コマンドを貼れば動きますが、どういう意図で書かれているか、その結果再現ができるようになることを目標にしています。
また、本記事で使用するソフトウェアはすべて無料であり、Linux標準の仕組みで使えるものはなんでも使っています。
検証環境
本記事の内容は、次の環境で実際に検証しています。
| 項目 | 使用環境 |
|---|---|
| ホストPC | Windows 11 Pro |
| クライアント | iPad 第10世代(Safari) |
| 動作確認 | iPhone Safari |
| Raspberry Pi | Raspberry Pi Zero WH |
| OS | Raspberry Pi OS Lite(Bookworm / Trixieで検証) |
| キャプチャカード | UVC対応 USB HDMIキャプチャ |
| USB OTG変換アダプター | microUSBオス to USB-A |
| 使用ソフト | FFmpeg / v4l-utils / Python3標準ライブラリ |
| Webサーバー | Python http.server |
| 接続 | USB LAN・Wi-Fiの両方で確認 |
| 電源 | コンセントから直接取ると安定します。 |
設計思想
「Raspberry Pi OSをインストールした直後でも、数個のパッケージ追加だけで再現できる」これを設計の最優先事項とします。
- FFmpeg:HDMIキャプチャカードから映像を取得し、静止画を生成するを生成するコンポーネント
- v4l-utils:キャプチャデバイスの能力の確認、適切な設定値を決めるツール
- Python標準ライブラリ(http.server):HTTPサーバーを構築するために仕様
最小の構成で再現性を高めつつ、安定してブラウザのみで運用できることを目標にしました。
再現性を最優先にすること
Linuxでは便利なソフトウェアが数多く公開されています。しかし利用するライブラリが増えれば増えるほどに困ったこともでてきます。
- バージョンに依存する
- 更新による設計の見直し
- インストールの手順
- 場合によっては削除したのにautoremoveをしなければ残り続ける断片
これらはあまり好ましくないと考えています。そのため、Raspberry Pi OS標準で揃えることを目標にしました。これならば、全ての書き直しということにはならないと思いました。
数年後も使えるように比較的再現しやすい方法を選びました。
完成版の構築手順
ここからはVersion 1.0の構築を実際に行った順番で記録します。
記事執筆時点(2026年8月3日)の検証環境は次の通りです。
- Raspberry Pi Zero WH
- Raspberry Pi OS Lite 32bit Trixie
- FFmpeg
- v4l-utils
- Python標準ライブラリ
これだけです。この環境以外では動作が異なる可能性があります。ご容赦お願いします。
完成版の実際の手順
何回かWindowsの.sshフォルダの中身を消して、microSDカードを消去し、OSを入れ直して手順通りにりましたが、問題なく動きました。
STEP 0:システムを最新状態へ更新する
sudo apt update
sudo apt -y full-upgrade
sudo reboot
新しくOSをインストールした際のおまじないのようなものです。rebootはカーネル更新が含まれていた場合のためです。
Debian 13 Trixieは2025年と比較的新しいリリースのため、バグ修正が細かく入ります。EOI missingのような症状がこれらでrebootした後に直ることもあるため、再現性を求める上では必須と言えます。
STEP 1:必要パッケージの追加
sudo apt install -y ffmpeg
sudo apt install -y v4l-utils
STEP 2:作業ディレクトリの作成
mkdir -p ~/PaperDisplay/www
cd ~/PaperDisplay
wwwディレクトリを作り分離している意味は、Python標準のhttp.serverがカレントディレクトリ以下を全て公開する仕様のためです。
/wwwには後のcapture.jpgやindex.htmlだけをいれ、見せたくないものは見せないようにしています。
STEP 3:キャプチャカードの確認
v4l2-ctl --list-devices
v4l2-ctl -d /dev/video0 --list-formats-ext
後述します。
STEP 4:キャプチャー用スクリプト(デコードも再エンコードもなし)常駐型
sudo nano ~/PaperDisplay/capture.sh
#!/bin/bash
exec /usr/bin/ffmpeg \
-f v4l2 \
-input_format mjpeg \
-video_size 1024x768 \
-framerate 5 \
-i /dev/video0 \
-c:v copy \
-update 1 \
-y \
/home/pi/PaperDisplay/www/capture.jpg \
-loglevel error
オプションがとても長いので主要なものだけを表にして載せます。
| オプション | 役割 | Version 1.0での意義 |
|---|---|---|
| -framerate 5 | デバイス側に5fpsで出力要求 | 負荷対策 |
| -c:v copy | 入力をそのまま出力コピー | デコード・エンコードの回避 |
| -update 1 | ファイルの上書き明示 | 連番ファイルではないと明示 |
| -y | 出力ファイルの上書き省略 | 忘れると大変なことになる |
| /home/pi/…/capture.jpg | 出力先 | Safari側がポーリングする対象 |
chmod +x ~/PaperDisplay/capture.sh
chmod +xはファイルに実行権限を与えるコマンドです。これがないとcapture.shが実行できません。+xは全員が使えるという意味です。umaskの影響で権限が変わるため気を付けたいです。
STEP 4:キャプチャーサービス化
sudo nano /etc/systemd/system/paperdisplay-capture.service
[Unit]
Description=PaperDisplay Capture
After=network-online.target
[Service]
Type=simple
User=pi
Nice=10
WorkingDirectory=/home/pi/PaperDisplay
ExecStart=/home/pi/PaperDisplay/capture.sh
Restart=always
RestartSec=2
[Install]
WantedBy=multi-user.target
STEP 5:HTTPサーバーのサービス化
sudo nano /etc/systemd/system/paperdisplay-http.service
[Unit]
Description=PaperDisplay HTTP Server
After=network-online.target
[Service]
Type=simple
User=pi
Nice=10
WorkingDirectory=/home/pi/PaperDisplay/www
ExecStart=/usr/bin/python3 -m http.server 8000
Restart=always
RestartSec=2
[Install]
WantedBy=multi-user.target
| オプション | 役割 | Version 1.0での意義 |
|---|---|---|
| Type=simple | 起動したら即起動完了とみなす | FFmpegはデーモン化しない |
| Nice=10 | CPUスケジューリングを下げる | Zeroは単コアなので他に譲る |
| Restart=always / Restart=2 | 落ちたら2秒後に再起動 | 実際に起きた無限ループ回避 |
| -y | 出力ファイルの上書き省略 | 忘れると大変なことになる |
| /home/pi/…/capture.jpg | 出力先 | Safari側がポーリングする対象 |
python3 -m http.serverは標準ライブラリであるため、外部ライブラリを一切追加せずに静的ファイル配信サーバーを立てることができる。
STEP 6:表示ページ作成(HTML形式)
sudo nano ~/PaperDisplay/www/index.html
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>html,body{margin:0;background:#000;height:100%;overflow:hidden}
img{width:100%;height:100%;object-fit:contain;display:block}</style>
</head>
<body>
<img id="s"><script>
const img = document.getElementById('s');
let cur = null;
async function tick(){
try{
const res = await fetch('capture.jpg?t=' + Date.now(), {cache:'no-store'});
const blob = await res.blob();
const url = URL.createObjectURL(blob);
img.src = url;
if(cur) URL.revokeObjectURL(cur);
cur = url;
}catch(e){}
setTimeout(tick, 200);
}
tick();
</script>
</body>
</html>
WordPressばかり使っていたので久しぶりにHTMLを書いた気がします。削りに削ってここでギリギリ運用できるようになりました。
STEP 7:有効化、起動、確認
sudo systemctl daemon-reload
sudo systemctl enable --now paperdisplay-capture.service
sudo systemctl enable --now paperdisplay-http.service
systemctl status paperdisplay-capture.service
systemctl status paperdisplay-http.service
top -b -n 3 -d 1 | grep ffmpeg
STEP 8:再起動からの最終確認
sudo reboot
systemctl status paperdisplay-capture.service
systemctl status paperdisplay-http.service
journalctl -u paperdisplay-capture.service -n 30
STEP 9:iPadのSafariで確認
http://ホスト名:8000
http://Raspberry PiのIPアドレス:8000
STEP 10:Windows側の操作
Win + P → 複製・拡張
補足:パッケージの役割
【FFmpeg】
役割:動画・音声の変換、キャプチャ、配信となんでもできる汎用マルチメディア処理ツール。他の記事でも便利に使わさせていただいています。
今回はUVCキャプチャカードからのV4L2デバイスの読み取り、MJPEGフレーム取得、静止画ファイルへの書き出しのすべてを担う無くてはならないコンポーネント。-c:v copyによる再エンコードゼロ化運用をすることができたため、Raspberry Pi Zero WHの非力なCPUでも実用的なものにできた。
【v4l-utils】
Video4Linux2デバイスの情報取得・制御を行うコマンドツールたち。
FFmpeg単体では、デバイスが実際にどの解像度やフレームレートに対応しているか、これを事前確認する手段がありません。そのためv4l2-ctl –list-formats-extによってキャプチャカードの限界値を把握できる。必須ではないけれど、トラブルシューティングとしては不可欠でした。
【python3標準ライブラリ】
http.serverモジュールの最小構成の静的ファイルサーバーとして有能でした。
Apacheやnginxのような本格的なWebサーバーを導入することが私にはできませんでした。そこでファイルをHTTP経由で配る方法を採用しました。設定ファイルの記述もほぼゼロで済み、Raspberry Pi Zero WHのストレージとメモリ消費を最小限に抑えられました。
構成の軽量化を追い込む理由について
ここまで構成を追い込んだ理由は最小限にこだわったからだけではありません。
開発初期には、より複雑な構成や複数の実装方法を使いました。しかし、Raspberry Pi Zero WHという限られたCPUやメモリ資源では、不要な処理を一つ増やすだけで体感速度が大きく低下したり、動かなくなったりしました。
そこでなんどもエラーを吐き出しながら必要なものだけを残す方法に考えを切り替え、試行錯誤を繰り返した結果、最終的に現在の3つにせずを得なければなりませんでした。
軽量化は思わぬ副産物も生み出しました。
- 将来のメンテナンスが簡単
- Raspberry Pi OSの標準環境に近い
- 再現性の高さ
- 環境による違い、いわゆる「おま環」に悩まされない
設計思想を変えたことで、できるだけ依存を増やさず、長く使える構成をめざす方向へ目指す結果となりました。
最初に考えた構成と失敗
最初から完成形の構成を目指していたわけではありません。手をつけた初日は「静止画を書き換えればそれっぽくなるだろう」という楽観的な構成を考えていました。
FFmpeg → capture.jpg → Python HTTP Server → safari
実際、この構成でも画面は表示されました。ただ数時間動かして検証をしているうちに、多くの問題が見えてきました。
「表示できる」ことと「運用できる」ことの間には大きな溝がありました。
問題 1:CPU使用率、効率が悪い
当初はFFmpegでJPEGへ再エンコードしていました。画質も必要十分で一見すると問題はありません。
しかし、問題は画質とかそういうレベルではありませんでした。Raspberry Pi Zero WHのCPU使用率が90%以上に張り付き、SSHの操作すら重くなる惨状になりました。私はCPU使用率は高い方が良いと思っています。しかしそれは意図した状態である場合に限ります。今回は明らかにリソースのフル活用という状態ではありませんでした。
映像変換そのものがCPUを使い切っていて、他のことは何もできない状態でした。
問題 2:SSHを閉じると止まる
当初はあまり考えずにSSHからpython3 -m http.serverとFFmpegを起動して動かしていました。そのためSSHを閉じるだけで全てが終了してしまいました。
一時的な実験ならば問題ないとおもいましたが、常時表示できるものと考えていたため、実用化することができませんでした。
問題 3:Safariは想像よりも癖が強いブラウザでした
Apple関係のデバイスやアプリは「絶対に触らせない」という強い意志を持ったものがほとんどです。もちろんSafariもそうでした。
MJPEGストリームをそのまま配信することで解決できるかと思いました。Chromeでは問題なく表示されていました。しかし同じ環境でもSafariでは期待通りには動かず、ダウンロード画面になったり、画像が正常に描写されなかったりしました。
Safari独自の何かがあると思ったため、仕様について勉強しなくてはならなくなりました。
問題 4:長時間動かしていると止まってしまう
while true → FFmpeg → JPEG生成 → 終了 → 再実行
この構成で動かしていました。しかしこの方法は毎回キャプチャデバイスを開閉していたようで、V4L2関連のエラーが蓄積していきました。
2時間、短ければ15分くらいで停止します。原因がわからず熱くなっていた安物のキャプチャカードが故障したのかと疑いました。
結局はFFmpegを一度起動し、プロセスを常駐させる構成へ変更することで、突然停止の問題を回避できました。
これらの問題から見えてきたこと
検証に半日以上かけて、なんどもFAT32にフォーマットしてOSを入れ直してを繰り返してわかったことは、問題は大きくしぼって4つ、そしてそれらは単独の問題であり原因を切り分けることで完成させることができるということでした。
この構成で解決しなくてはならない4つの問題とは次の通りです。
- CPU負荷の軽減
- プロセス管理
- Safariの謎仕様の理解
- Linuxの運用の見直し
ここまでくれば改善するだけです。
改善したポイント
半日かけて問題を洗い出した結果、問題の切り分けに成功しました。その結果、かなり良い具合に改善することができ完成させることができました。
おもな初期構成とVersion 1.0の違いについて表にします。
| 項目 | 初期構成 | Version 1.0 |
|---|---|---|
| FFmpeg | JPEGへ再エンコード | -c:v copyによるコピー |
| フレームレート | 10fps固定 | キャプチャカードに合わせ5fpsへ最適化 |
| FFmpeg起動 | 手動 | systemdで常駐 |
| HTTPサーバー | 手動 | systemdで常駐 |
| ブラウザ更新 | img.srcを書き換え | fetch + Blob |
| メモリ管理 | 解放なし | revokeObjectURL |
| プロセス管理 | SSH依存 | 自動復旧 |
| CPU負荷 | 非常に効率が悪い | 大幅改善 |
表にしてみるとマイナーチェンジのように思えてきましたが結構かわりました。システム全体の設計が変わったと思います。
改善 1:「処理をする」から「処理しない」へ変更
初期構成ではFFmpegでJPEGへ再エンコードしていました。画質は十分でしたがそのせいでRaspberry Pi Zero WHのCPUは90%以上に張り付く結果となりました。
意図通りであるならば、CPU使用率が100%に近ければ近いほどリソースのフル活用状態で良いと思っています。しかし、今回の場合は明らかにシステムの限界に達しており余裕がない状態です。
問題を切り分けたところ、単純なことですが、キャプチャカードは最初からMJPEGを出力していたことがわかりました。
つまり、一度JPEGへ展開しもう一度JPEGへ圧縮する、この作業が不要でした。
そこで-c:v copyへ変更するとFFmpegはコピーするだけになり、CPU使用率は劇的に改善されました。条件次第では30%未満になることもあります。
Version 1.0での一番の改善点はこの部分、FFmpegの仕事を減らしたことです。
改善 2:Raspberry Piへ運用を任せる
初期構成では、SSHでログイン → HTTPサーバー起動 → PowerShellの別タブでSSH接続 → FFmpegを起動、このような流れでした。
見ての通りですがマンパワーで一つひとつ行っていました。試験運転ならばこれでいいと思います。
しかし、使用していくと致命的な問題が3つありました。
- SSHが切れるだけで停止
- Windowsがスリープに入るだけで停止
- ネットワークが一瞬途切れても停止
これでは常に監視をしていなくてはならず、運用するには難しいと思い、systemdへ移行しました。
これにより起動から停止までLinux自身が管理するようになります。この変更は自動運用という面においては大きな改善でした。ただし、実際に動かして方向性が定まらないうちは導入できませんでした。
改善 3:Safariの仕様へ合わせる
当初の設計ではMJPEGストリームをそのままブラウザへ流そうとがんばりました。色々なブラウザでは問題はありませんでしたが、肝心のSafariではダウンロード扱いになったり画面が更新されなかったり、ブラウザ側の仕様が違いすぎることに問題がありました。
そこで発想を変え、動画ではなく静止画をパラパラ漫画のように高速更新する方式へ変更しました。
Safariは通常の画像表示が得意なようで、この方法ならば特別なことをする必要もありません。
導入にもある通り、この記事の設定は全てSafari特化型となっています。
改善 4:長時間動けるように改善する
試験運用のつもりで組んだので数分動いて喜んでいました。しかし、実際に運用と考えると数分で使えなくなるサブディスプレイには何の価値もありません。
最低でも数時間、できれば一日中動き続けてほしいものです。
問題となるのはFFmpegを毎回起動していた構成でした。デバイスを毎回起動するため、長時間ではエラーの蓄積がひどいものでした。
そこでFFmpegを一度だけ起動し、同じプロセスが画像を書き換え続ける方式へ変更しました。これは安定性に大きく貢献した改善になったと思います。
結局何をやっているのか?
ここまでいろいろなコマンドを並べて、なんだかすごそうなことをしている感じを出していましたが、結局何をしているかというとシンプルなものです。
「Windowsの画面を0.2秒ごとに写真へ取り直し、それをブラウザへ表示している」
画面を高速で撮影し続け、ブラウザがその画面を更新しているだけです。
仕組みが単純であるため、Safariで追加アプリなしの状態で表示することができます。
新しいアルゴリズムを開発したわけでも、新しいソフトウェアを書いたわけでもありません。使っている技術はFFmpeg、HTTP、HTML、JavaScript、Pythonと標準的なものだけです。
「すごい技術でWindowsの画面をiPadに映している」のではなく「標準的な技術の組み合わせでWindowsの画面をiPadへ映している」
よくある質問(FAQ)
- QRaspberry Pi Zero 2 WやPi 3系、Pi 4、Pi 5でも動作しますか?
- A
はい。性能の低いRaspberry Pi Zero WHを使い検証をしました。高性能な機種であれば問題なく動きます。
- QAndroidタブレットでも利用できますか?
- A
今回の検証はSafariだけに限定した仕組みではありません。HTTPで配信された画像を表示できるブラウザであれば、基本的な仕組みは同じです。ただ、Kindle paperwhiteのようにメモリが
- Q動画視聴やゲーム用途にも使えますか?
- A
使い物になりません。動画ストリーミングではなく、画像を高速で更新する方式です。遅延がひどすぎるため動的な目的で使うことは難しいです。
- QなぜMJPEGストリーム方式を採用しなかったのですか?
- A
今回の検証はソフトウェアを入れずにiPadをWindowsのサブディスプレイにすることで、Safariでの利用を想定していました。しかしブラウザ内で再生されず、ダウンロード扱いになるなど問題がありました。性能よりも互換性と再現性を優先した結果、静止画の定期的な取得に落ち着きました。縛りがなければMJPEGストリーム方式の方が正解だと思います。
- QなぜPython標準ライブラリだけを使ったのですか?
- A
再現しやすい環境を作りたかったからです。WebサーバーにはApacheをはじめ優れたものがあります。ライブラリが増えるほど設定やバージョン依存そしてトラブルシューティングなど複雑になります。最小パッケージだけで再現できることを重視しました。
まとめ
「専用ソフトウェアをインストールせずにiPadをWindowsのサブディスプレイとして使いたい」という話を聞き、実験してみようかなと思ったことがきっかけでした。
当初は「うまくいったらおもしろいな」程度の考えでしたが、実際に行動に移すと、ブラウザの仕様やLinuxのプロセス管理など多くの課題に直面しました。
課題を一つひとつ考えてみると、ほぼ全て独立した問題で切り分けが容易でした。最終的にはFFmpeg・v4l-utils・Pythonとシンプルにした方が良い結果が得られるというおもしろい結論に達しました。
2018年の低スペックなRaspberry Pi Zero WHでも設計を工夫すれば新しい役割を果たすことができる、ということを実証できたことは良かったと思います。
リファレンス
FFmpeg Documentation.
https://ffmpeg.org/documentation.html
FFmpeg Devices Documentation (Video4linux2).
https://ffmpeg.org/ffmpeg-devices.html#video4linux2_002c-v4l2
Raspberry Pi Documentation.
www.raspberrypi.com/documentation/
Python Software Foundation (http.server).
https://docs.python.org/3/library/http.server.html
MDN Web Docs (Fetch API).
https://developer.mozilla.org/docs/Web/API/Fetch_API
MDN Web Docs (URL.createObjectURL())
https://developer.mozilla.org/docs/Web/API/URL/createObjectURL
MDN Web Docs (URL.revokeObjectURL())
https://developer.mozilla.org/docs/Web/API/URL/revokeObjectURL
WebKit Project.
https://webkit.org/

コメント