diff --git a/.github/ISSUE_TEMPLATE/device_report.yml b/.github/ISSUE_TEMPLATE/device_report.yml index f50ea383..938a5768 100644 --- a/.github/ISSUE_TEMPLATE/device_report.yml +++ b/.github/ISSUE_TEMPLATE/device_report.yml @@ -6,7 +6,7 @@ body: - type: markdown attributes: value: | - Thanks for testing! Reports are collected into the **Verified devices** table in the README. + Thanks for testing! Reports are collected into the **Verified devices** table in [docs/devices.md](https://github.com/OpenIPC/decoder/blob/v2/docs/devices.md#verified-devices). The app version is shown in the menu next to **Exit**, for example `v2.02`. 感谢测试!可以用中文填写。应用版本显示在菜单中 **Exit** 旁边。 diff --git a/README.md b/README.md index e256074a..755e2028 100644 --- a/README.md +++ b/README.md @@ -11,38 +11,49 @@ A small Android app for watching RTSP streams from IP cameras, with hardware H.264/H.265 decoding. It is built for cameras running [OpenIPC](https://openipc.org) firmware and works on phones, tablets, TVs, set-top boxes and car devices. +- **Video:** H.264/H.265 via MediaCodec, MJPEG over RTSP and HTTP; automatic decoder recovery +- **Audio:** AAC, Opus, G.711, L16 +- **Views:** one camera, or multi-view with up to 8 cameras plus a camera that pushes RTP, decoding adapted to what the device can do +- **Network:** RTSP over TCP, UDP (Auto falls back to TCP) or multicast, RTP pushed by the camera (Majestic `outgoing`), Basic and Digest authentication, automatic reconnection +- **More:** Android TV remote, zoom, camera web interface, URLs stored encrypted, a debug page on port 8085 +

Multi-view with eight cameras and the menu

+

+ Single view right after connecting: transport, codecs and bitrate +

+ +

+ The menu on a phone + WebUI on a phone: the page scrolls the field above the keyboard +

+ +

+ Debug page: snapshot with each cell's text
+ /stats.json + /log +

+ ## Download Ready-to-install builds are in the [**latest** release](https://github.com/OpenIPC/decoder/releases/tag/latest): -| File | What it is | -|------------------|----------------------------------------| -| `decoder-v2.apk` | **Current version**, recommended | -| `decoder.apk` | Legacy version from the `basic` branch | +| File | What it is | +|---|---| +| `decoder-v2.apk` | **Current version**, recommended | +| `decoder.apk` | Legacy version from the `basic` branch | -Each 2.xx version also has a release of its own — [`v2.01`](https://github.com/OpenIPC/decoder/releases/tag/v2.01) and later, on the [releases page](https://github.com/OpenIPC/decoder/releases) — with that version's APK and the changes since the previous one. +Each 2.xx version also has a release of its own on the [releases page](https://github.com/OpenIPC/decoder/releases), with that version's APK and the changes since the previous one. A beta channel is available on [Google Play](https://play.google.com/apps/internaltest/4701231141824898735). To join it, send your e-mail through the [OpenIPC Telegram channels](https://openipc.org/our-channels). -## Features - -- **Video:** RTSP playback, hardware H.264/H.265 decoding via MediaCodec, MJPEG over RTSP (RTP/JPEG) and over HTTP (`multipart/x-mixed-replace`) in the single view and in multi-view, automatic decoder recovery, a lossy UDP stream shown with concealed damage instead of a black picture, a warning when the device's video decoder stops responding, picture fitted to any screen shape -- **Audio:** AAC (RFC 3640 `mpeg4-generic`, ADTS, MP4A-LATM), Opus, G.711 μ-law/A-law, L16 PCM (mono/stereo); in multi-view, the sound of the selected cell -- **Display:** single camera, or multi-view (Quad) with up to 8 cameras plus the U cell (a camera that pushes RTP) in a 2×2 or 3×3 grid; adaptive decoding (full rate or key frames per cell, learnt per device); S: key frames only, in multi-view and in the single view -- **Controls:** pinch-to-zoom, pan, double-tap to reset, Android TV remote (Leanback launcher) -- **Network:** RTSP over TCP or UDP (Auto, the default, falls back to TCP when UDP is refused, brings no video or keeps losing key frames), the transport in use shown on connect, RTSP multicast, plain RTP pushed over UDP by the camera (unicast or multicast), Basic and Digest authentication, RTSP keepalive, automatic reconnection with exponential back-off, connection-quality indicator -- **WebUI:** built-in browser for the camera's web interface -- **More:** 8 camera slots, URLs stored encrypted, long-press screenshot, URLs can be shared from other apps - ## Requirements - Android 5.0 (API 21) or higher -- An IP camera with RTSP, or one that pushes RTP over UDP (OpenIPC Majestic `outgoing`), see [Transports](#transports) -- Google services (GAPPS) are **not** required: the app works on AOSP builds without them. WebUI uses the system WebView, so it needs a working WebView package (on some devices this is Google's *Android System WebView*); disabling it affects only WebUI — [#15](https://github.com/OpenIPC/decoder/issues/15) +- An IP camera with RTSP or HTTP MJPEG, or one that pushes RTP over UDP (OpenIPC Majestic `outgoing`) +- Google services (GAPPS) are **not** required. WebUI needs a working system WebView ([#15](https://github.com/OpenIPC/decoder/issues/15)) ## Quick start @@ -52,125 +63,27 @@ A beta channel is available on [Google Play](https://play.google.com/apps/intern ``` rtsp://username:password@ip_address:554/stream=0 ``` - Under the URL you can choose the [transport](#transports); Auto (UDP, falling back to TCP) is the default for a new slot. + Under the URL you can choose the transport; Auto (UDP, falling back to TCP) is the default for a new slot. Other URL types: [Transports](docs/transports.md). 4. Press **Save**. The app connects straight away and reconnects automatically if the stream drops. ## Controls -### Menu - -The menu is a grid: the eight slots on top, two rows of four below. - ``` [ 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 ] [ Settings | WebUI | Grid | Slides ] [ Exit | Reset | Debug | UDP ] ``` -

- The menu on a phone - WebUI on a phone: the page scrolls the field above the keyboard -

- -| Item | Action | -|--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| **1**–**8** | Switch to a camera slot. The active slot's number shows the connection quality: green under 100 ms of jitter, yellow 100–300 ms, red above 300 ms. A grey number is an empty slot. | -| **Settings** | Edit the URL and the transport of the active slot (single view only; grey elsewhere) | -| **WebUI** | Open the camera's web interface; credentials come from the RTSP URL (single view only; grey elsewhere) | -| **Grid** | The view: multi-view (2×2 or 3×3) or the single camera. Grid does not change the playback mode (Slides) | -| **Slides** | The playback mode: every frame, or key frames only (the picture updates once per key frame interval, MJPEG once a second), the lowest decoder load. Works in multi-view and in the single view and never changes the view | -| **Exit** | Close the app. The small text under it is the app version and build hash. | -| **Reset** | Clear all settings after a confirmation (**Cancel** has the focus) to set the app up again from scratch: camera slots, transports and the other settings are removed, and the app opens as right after installing. The cameras are not changed | -| **Debug** | Text in the corner of every multi-view cell (resolution, codec, decoded/received fps, key frames, status), or in none | -| **UDP** | The U cell full-screen: the camera that pushes RTP over UDP to port 5600 | - -Items that are on — Grid, Slides, Debug, UDP and the active slot — have an orange fill; the item the remote is on has a light blue frame. - -### Gestures and keys - -| Input | Action | -|--------------------------|------------------------------------------------------------------------------------------| -| Tap / **OK** / **Enter** | Open or close the menu | -| **Back** / **Esc** | Close the menu | -| Pinch, then drag | Zoom (up to 5×) and pan, single view | -| Double tap | Reset zoom | -| Long press | Single view: save a screenshot to `Pictures/OpenIPC`. Multi-view: play this cell's sound | - -### Opening a camera from another app - -The app accepts plain-text shares (`ACTION_SEND`, `text/plain`). Sharing an `rtsp://`, `udp://` or `http://` URL from another app, for example OpenIPC Network, puts it into the active slot. An optional `transport` extra (`tcp`, `udp`, `auto`, `multicast`) sets the slot's transport: - -```sh -adb shell am start -a android.intent.action.SEND -t text/plain \ - --es android.intent.extra.TEXT 'rtsp://user:pass@192.168.1.10/stream=0' \ - --es transport udp -n com.openipc.decoder/.Decoder -``` - -### Multi-view (Quad) +| Input | Action | +|---|---| +| Tap / **OK** | Open or close the menu | +| **1**–**8** | Camera slot; the number's colour shows the connection quality | +| **Grid** / **Slides** | Multi-view or one camera / every frame or key frames only | +| **Debug** / **UDP** | Text in the multi-view cells / the camera that pushes RTP to port 5600 | +| Pinch, double tap | Zoom and pan, reset the zoom (single view) | +| Long press | Screenshot (single view) / play this cell's sound (multi-view) | -Shows the configured slots in a 2×2 grid, or in a 3×3 grid once any of slots 5–8 is used. The ninth cell of the 3×3 grid, the **U** cell, plays a camera that pushes RTP over UDP to port 5600 of the device (`udp://@:5600`, see [udp:// slots](#udp-slots-majestic-outgoing)); until a stream arrives it shows every address of the device to send to. **UDP** in the menu opens it full-screen. Other ports or multicast groups go into a slot as a `udp://` URL. Each cell has its own connection and decoder, and the device's video decoder has a fixed budget: the AG-216 test box, for example, decodes about two 1080p30 streams at full rate. So the grid adapts: cells start on key frames only, then decode every frame in priority order (the U cell while it receives a stream, the cell whose sound plays, then slot order) while the decoder keeps up. Cells beyond that stay on key frames, with the picture updated once per key frame interval (`key frames` in the corner text, menu **Debug**). The limit is learnt on each device by measurement. Substreams of the cameras, where available, let more cells play at full rate. A key-frame cell over the hardware decoder's instance limit (the AG-216 has 4 × H.265) uses a software decoder instead (`· software` in the corner); only beyond that budget too does it show `Decoder limit: N × H.265`. When even key frames are too much, the lowest-priority cell is paused (`Paused: decoder overloaded`) and tried again after a quiet period; losses in the first seconds after the grid starts, when every decoder starts at once, do not count. A cell whose UDP stream loses a piece of every key frame shows the frames with the damage concealed (`· damaged`) rather than a black picture. Some cameras accept only one RTSP client at a time. Long-press a cell to play its audio (long-press it again to mute); the selected cell shows an AUDIO badge and is remembered. Tap any cell to open the menu. - -## Transports - -Each slot has its own transport, chosen under the URL in **Settings** (or with the `transport` extra of a shared link): - -| Transport | How the video arrives | Notes | -|-------------------|----------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------| -| **TCP** | RTP inside the RTSP connection | Works everywhere, also through NAT and firewalls | -| **UDP** | RTP over UDP, a port pair per track | Lower latency on a LAN. Lost packets are skipped, not resent. Never falls back | -| **Auto** (default) | UDP, or TCP when UDP does not work | Falls back to TCP when the camera refuses UDP or no video arrives within 3 s, then stays on TCP until the slot is changed | -| **Multicast** | RTSP multicast (Android 8.0+), or TCP when it is refused | For servers that offer RTSP multicast. OpenIPC Majestic and XM cameras do not: they reply 400 or 451, and the slot plays over TCP in the same connection | - -The transport actually in use is shown when a stream starts in single view (`Connected • UDP • H.265 …`), on the debug page and in `/stats.json` (`transport`; the slot's setting is `transport_mode`). After a fallback it reads, for example, `TCP (auto)`. - -

- Single view right after connecting: transport, codecs and bitrate -

- -### UDP slots (Majestic outgoing) - -A slot can also receive RTP that the camera pushes by itself, without RTSP. Put a `udp://` URL into the slot instead of `rtsp://`: - -| URL | Meaning | -|-------------------------------------|----------------------------------------------------------------------| -| `udp://@:5600` | Listen on port 5600 | -| `udp://@239.1.2.3:5600` | Join the multicast group 239.1.2.3 on port 5600 | -| `udp://192.168.1.10@239.1.2.3:5600` | The same, but accept packets from this sender only | -| `udp://@:5600?codec=h265&pt=97` | Set the codec or the video payload type when the defaults do not fit | - -The codec is taken from the RTP payload type (96 → H.264, 97 → H.265) unless `codec=` is given, and a codec change on the camera is followed while playing. Video only. Two slots cannot listen on the same port. - -On the camera, add the device as an `outgoing` destination in `/etc/majestic.yaml` (then restart Majestic): - -```yaml -outgoing: - servers: - - udp://192.168.1.20:5600 -``` - -`/api/v1/outgoing.json` on the camera shows whether the destination is live. Majestic can also send to a multicast group (`udp://239.1.2.3:5600`), so several devices can watch one camera. On Wi‑Fi the app holds a multicast lock while it is in a group. - -### HTTP MJPEG slots - -A slot also takes an HTTP MJPEG URL, such as Majestic's `http://user:password@/mjpeg`: a `multipart/x-mixed-replace` stream with one JPEG per part, chunked or not. Basic credentials are sent with the request, Digest after the camera asks for it; the stream reconnects like RTSP. It plays in the single view and in multi-view; the transport shows as `HTTP`. There is no audio on this path. - -## Debug page (port 8085) - -While the app runs it serves its debug information over HTTP on port **8085**, for a browser or `curl` on the same network: - -- `http://:8085/` — the snapshot fills the page, with each cell's camera, size, fps and losses (in red) drawn over it; the header shows the version, the device time and uptime. **Info On / Off** in the header hides the text over the cells (and the list below them on a narrow screen), leaving only the picture; it is **Off** until switched on, and the choice is remembered in the browser. A red note in the header says when the device's video decoder stopped responding (`decoder_stuck`); -- `http://:8085/snapshot.jpg` (`?labels=0`: frames only) — a picture laid out like the screen, at the screen's resolution (at most 1920×1080), made from the frames the app shows right now; a cell without video is black with its status. Taken only when asked for; the page refreshes it every 3, 5 (default) or 10 s while the tab is visible; -- `http://:8085/stats.json` — the same as JSON, for scripts; -- In single view the page and the snapshot still show the whole grid (#100): the camera on screen is framed in blue, every other configured slot gets a short stream check (connect, a few seconds of packets, no decoding) about every 45 s while the page is open, and its cell says what was found, e.g. `Stream OK · H.265 · 25 fps · 3.1 Mbit/s · checked 12 s ago`, or the error. A slot of the camera on screen is never checked (some cameras take one client only), nor a `udp://` slot. `stats.json` has these cells under `slots`; -- `http://:8085/log` — the last 500 lines of the app's log. - -Only clients on the local network are answered (loopback, 10/8, 172.16/12, 192.168/16, link-local, IPv6 fc00::/7); others get 403. Logins and passwords are never shown. - -

- Debug page: snapshot with each cell's text
- /stats.json - /log -

+Every item, the remote, sharing a URL from another app and **Reset**: [Usage](docs/usage.md). ## Known limitations @@ -179,140 +92,39 @@ Only clients on the local network are answered (loopback, 10/8, 172.16/12, 192.1 > > - **RTSP multicast needs Android 8.0+** and a server that offers it; OpenIPC cameras do not — [#69](https://github.com/OpenIPC/decoder/issues/69) > - **G.726 audio and MP4A-LATM with in-band configuration (`cpresent=1`) are not supported.** OpenIPC Majestic does not use them, and cameras that offer G.726 can usually send G.711 or AAC instead — [#14](https://github.com/OpenIPC/decoder/issues/14) -> - **MJPEG is decoded in software on devices without a usable JPEG decoder** (the AG-216 lists one but cannot create it); hardware decoding is used where the device offers `video/jpeg` — [#132](https://github.com/OpenIPC/decoder/issues/132) -> - **The video decoder of the AG-216 can stop responding until the device is restarted**: frames arrive, none is decoded; the app says so on the screen and on the debug page, the cause is not known yet — [#145](https://github.com/OpenIPC/decoder/issues/145) -> - **Switching between multi-view and the single view can freeze the picture for seconds** on some TV boxes (seen on the AG-216) — [#76](https://github.com/OpenIPC/decoder/issues/76) +> - **MJPEG is decoded in software on devices without a usable JPEG decoder** (the AG-216 lists one but cannot create it) — [#132](https://github.com/OpenIPC/decoder/issues/132) +> - **The video decoder of the AG-216 can stop responding until the device is restarted**: frames arrive, none is decoded; the app says so on the screen and on the debug page — [#145](https://github.com/OpenIPC/decoder/issues/145) > - **Credentials are sent in clear text** over plain RTSP/HTTP (Basic auth unless the camera asks for Digest); RTSPS is not supported and not planned — [#16](https://github.com/OpenIPC/decoder/issues/16) +> - **Screenshots and the debug snapshot need Android 7.0+** > > Known bugs are tracked separately under the [`bug` label](https://github.com/OpenIPC/decoder/issues?q=is%3Aissue+is%3Aopen+label%3Abug). -## Branches - -| Branch | Status | -|---------|----------------------------------------------------------------------------------------------------------------| -| `v2` | **Main development branch** (default). Modular code in `android/`. Builds are published as `decoder-v2.apk`. | -| `basic` | Legacy single-file version (1.0x), kept for reference and older setups. Builds are published as `decoder.apk`. | - -Please base pull requests on `v2`. +## Documentation -## Building +| Document | Contents | +|---|---| +| [Usage](docs/usage.md) | menu, gestures and keys, remote control, multi-view, Slides, the U cell, WebUI, sharing a URL from another app, screenshots, Reset | +| [Transports](docs/transports.md) | URL formats and options: RTSP over TCP/UDP/Auto/multicast, `udp://` push (Majestic `outgoing`), HTTP MJPEG; codecs, authentication, what the servers support | +| [Debug](docs/debug.md) | debug page on port 8085, `/stats.json` field reference, `/log`, snapshots, what to send with a bug report | +| [Architecture](docs/architecture.md) | data path, classes, threads, the decode planner, SurfaceView rendering, repository layout | +| [Testing and releases](docs/testing.md) | branches, CI, unit tests, versions and releases, branch builds, `mapping.txt`, building locally | +| [Security](docs/security.md) | credential store, debug server (LAN only), log redaction, no encryption on the network | +| [Devices](docs/devices.md) | verified and not working devices, hardware decoder notes, device and camera reports | +| [Drones and Wi‑Fi toys](docs/toys.md) | how toys stream, FLOW-UFO (in testing), what to try with an unknown toy, what to send | +| [Laboratory](LABORATORY.md) | the test bench: devices, cameras, access, methods | -You need JDK 17 and the Android SDK (platform 36). Point the build at the SDK with `ANDROID_HOME` or `android/local.properties`. +The change history is in the release notes on the [releases page](https://github.com/OpenIPC/decoder/releases) (written by CI for every v2.xx version). -```bash -cd android -./gradlew assembleDebug # debug build, no signing keys needed -# → app/build/outputs/apk/debug/app-debug.apk -``` - -A release build needs a keystore at `android/key.jks` and these environment variables: +## Support -```bash -export KEYSTORE_PASS=your_password -export KEY_ALIAS=your_alias -export KEY_PASS=your_key_password -./gradlew assembleRelease # → app/build/outputs/apk/release/app-release.apk -``` +- Questions and bug reports: [GitHub Issues](https://github.com/OpenIPC/decoder/issues) +- Tested the app on a device or a camera? Please send a [Device report](https://github.com/OpenIPC/decoder/issues/new?template=device_report.yml) or a [Camera report](https://github.com/OpenIPC/decoder/issues/new?template=camera_report.yml) +- OpenIPC: [openipc.org](https://openipc.org) -Debug and release builds are signed with different keys. To switch from one to the other, uninstall the app first; its saved camera URLs are removed with it. - -**CI:** every push is built by GitHub Actions. Pushes to `v2` and `basic` update the [latest](https://github.com/OpenIPC/decoder/releases/tag/latest) release and notify the OpenIPC Telegram channel. Other branches only attach the APK as a workflow artifact on the run page. - -## Architecture - -| Class | Role | -|------------------------------------------|-------------------------------------------------------------------------------------------------------------------| -| `Decoder` | Main activity: UI, lifecycle, settings, single-view video decoding, multi-view grid | -| `RtspClient` | Single-view stream: RTSP session, NAL assembly, audio, connection quality | -| `RtspSession` | One RTSP session over TCP, UDP or multicast: handshake, RTP receive, keepalive, watchdog, reconnect with back-off | -| `RtspAuth`, `Sdp` | Basic and Digest authentication; parsing of the camera's session description | -| `Transport`, `UdpReceiver`, `RtpReorder` | Transport negotiation; UDP and multicast receive with a sender filter; packet reordering | -| `RtpListener` | `udp://` slots: plain RTP pushed by the camera | -| `HttpMjpeg` | `http://` slots: MJPEG over HTTP (`multipart/x-mixed-replace`, chunked or not) | -| `NalAssembler` | Rebuilds H.264/H.265 NAL units from RTP packets, drops units damaged by packet loss | -| `JpegAssembler` | MJPEG: rebuilds whole JPEG frames from RTP/JPEG (RFC 2435) | -| `JpegCodec`, `JpegRenderer` | MJPEG on screen: the device's JPEG decoder where there is one, otherwise BitmapFactory | -| `DecodePlanner` | Multi-view: which cells decode every frame and which only key frames, within the learnt decoder capacity | -| `MediaCodecManager`, `DecoderRetry` | Hardware video decoder lifecycle and recovery after decoder errors | -| `AudioManager`, `AudioConfig` | Audio pipeline: G.711/L16 conversion, AAC/Opus decoding, AudioTrack playback | -| `QuadCell` | One multi-view cell: its own session, decoder and optional audio | -| `MenuManager` | Popup menu, URL editor, WebUI browser | -| `CredentialStore`, `KeystoreCipher` | Encrypted storage of camera URLs (Android Keystore) | -| `FramePool`, `Frame` | Buffer pool that reduces garbage-collector load | -| `App`, `Crash` | Uncaught-exception handler and crash report screen | -| `DebugServer`, `NetInfo`, `Snapshots` | Debug page on port 8085 (LAN clients only), device addresses, snapshots of the screen | - -Network, video decoding and audio run on separate threads, so a slow decoder never blocks the socket. - -## Security notes - -- Credentials are taken from the URL and sent in the `Authorization` header: Basic first, Digest when the camera asks for it. They are not included in the RTSP request line. -- RTSP, RTP over UDP and HTTP are not encrypted, and RTSPS is not supported. Credentials that a camera expects inside the URL path or query (for example `user=…_password=…`) are sent with every request. Use the app only on trusted networks, see [#16](https://github.com/OpenIPC/decoder/issues/16). -- Camera URLs, including passwords, are stored encrypted (AES-256-GCM with a key kept in the Android Keystore, Android 6.0+) in the app's private storage. On Android 5.x, or if the Keystore is unavailable, they are stored in plain text. Backup is disabled (`allowBackup="false"`). -- The debug page on port 8085 is not encrypted and has no password; it answers only local-network clients and never shows logins or passwords. - -## Device compatibility - -Tested the app on your device? Please [**submit a Device report**](https://github.com/OpenIPC/decoder/issues/new?template=device_report.yml). It takes a minute and helps other users. Tested a camera? [**Submit a Camera report**](https://github.com/OpenIPC/decoder/issues/new?template=camera_report.yml): manufacturer, model, RTSP URL (without the password), codecs and what works. - -### Verified devices - -Most devices below were reported with the legacy 1.0x version (`basic` branch); the *App* column shows the version that was tested. New reports should include the app version. - -| Type | Device | Android | Kernel | App | Notes | -|-------------|----------------------------------------|-----------------------------------|----------------|---------|--------------------------------------------------------------------------------------------| -| Phone | Asus ZC553KL | 8.1 | 3.18.71 | 1.0x | | -| Phone | Blackview BV4900Pro | 12 | 4.19.191 | 1.0x | | -| Phone | Oppo 5X | ColorOS 15.0 | 5.15.149 | 1.0x | | -| Phone | Oppo A17 | ColorOS 12.1 | 4.19.191 | 1.0x | | -| Phone | Samsung Galaxy A51 | 13 | 4.14.113 | 1.0x | | -| Phone | Samsung Galaxy M21 (SM-M215F/DSN) | 12 | 4.14.113 | 1.0x | | -| Phone | Samsung Galaxy S25 | 15 | 6.6.30 | 1.0x | | -| Phone | Samsung A55 5G | 14 | 6.1.93 | 1.0x | | -| Phone | Samsung Galaxy J7 | 10 | 3.18.150 | 1.0x | | -| Phone | Samsung S8 | 9 | 4.4.153 | 1.0x | | -| Phone | Samsung S23 Ultra | 15 | 5.15.153 | 1.0x | | -| Phone | TECNO KM4 | 15 | 5.15.194 | v2 2.04 | Lab touch device: WebUI with the on-screen keyboard checked ([#159](https://github.com/OpenIPC/decoder/issues/159), [#160](https://github.com/OpenIPC/decoder/issues/160)); Close under the status bar ([#169](https://github.com/OpenIPC/decoder/issues/169)) | -| Phone | Xiaomi Redmi 7A | 10 | 4.9.261 | 1.0x | | -| Phone | Xiaomi Redmi Note 7 (M1901F7G) | 10 | 4.4.192 | 1.0x | | -| Phone | Xiaomi Redmi Note 13 4G | 13 | 5.15.94 | 1.0x | | -| Tablet | Lenovo TB-8504X | 8.1.0 | 3.18.71 | v2 2.00 | H.265 and multi-view work; WebUI input fixed in 2.02 ([#159](https://github.com/OpenIPC/decoder/issues/159)) | -| Tablet | Lenovo TB-X304L | 8.1.0 | 3.18.71 | 1.0x | Picture cropped in 1.0x, fixed in v2 ([#17](https://github.com/OpenIPC/decoder/issues/17)) | -| Tablet | Lenovo TB-X606F | 10 | — | 1.0x | Picture cropped in 1.0x, fixed in v2 ([#17](https://github.com/OpenIPC/decoder/issues/17)) | -| Tablet | Lenovo Tab P11 | 11 | 4.19.157-perf+ | 1.0x | Picture cropped in 1.0x, fixed in v2 ([#17](https://github.com/OpenIPC/decoder/issues/17)) | -| Tablet | Samsung Galaxy Tab A | 7.1.1 | 3.10.49 | 1.0x | | -| Tablet | Samsung Galaxy Tab A | 11 | 4.9.227 | 1.0x | | -| Tablet | Samsung Tab A7 (SM-T505) | 12 | 4.19.157-perf- | 1.0x | | -| Set-top box | A95X F3 AIR | 9 (SlimBOXtv AOSP 9.14, no GAPPS) | 4.9.113 | 1.0x | | -| Set-top box | IE X3 Air | 9 (SlimBOXtv AOSP 9.14, no GAPPS) | 4.9.113 | 1.0x | | -| Set-top box | AG-216 | 10 | 4.9.170 | v2 2.00 | v2 test device; decoder budget measured at ≈ 1080p60 in total (two 1080p30 at full rate) | -| Set-top box | SberBox SBDV-00001 | custom | 4.9.228 | 1.0x | | -| Set-top box | X96Q (Allwinner H313) | 10 | 4.9.170 | 1.0x | | -| Projector | SIBIRLINK HY300 / HY320 | 11 | 4.19.323 | v2 2.02 | 1080p H.264 / H.265 work; 2304×1296 and 2560×1440 do not start ([#165](https://github.com/OpenIPC/decoder/issues/165), [#166](https://github.com/OpenIPC/decoder/issues/166)) | -| TV | Haier Candy Android TV 2K | 11 | 4.9.243+ | 1.0x | No icon in the device's app list | -| TV | Yandex TV Novex NVX-55U169TSY | 7.1.1 | 4.4.3 | 1.0x | | -| Car | Media Center JCAC10003 (ac8227/ac8229) | 12 | 3.18.79+ | 1.0x | | -| Car | Mirror Z55 | 8.1.0 | 4.4.83 | 1.0x | | - -### Not working - -| Type | Device | Android | Kernel | App | Notes | -|-------------|----------------|---------|---------|------|----------| -| Set-top box | Unnamed TV box | 5.1.1 | 3.14.29 | 1.0x | No video | - -## Version history - -Changes are listed on the [releases page](https://github.com/OpenIPC/decoder/releases) and in the [commit history](https://github.com/OpenIPC/decoder/commits/v2). The history of 1.0x versions is in the [`basic` branch README](https://github.com/OpenIPC/decoder/tree/basic#version-history). +[![Telegram](https://openipc.org/images/telegram_button.svg)](https://openipc.org/our-channels) ## License [The Prosperity Public License 3.0.0](LICENSE.md): free for noncommercial use, with a 30-day trial for commercial use. See [LICENSE.md](LICENSE.md) for the full terms. -## Support - -- Questions and bug reports: [GitHub Issues](https://github.com/OpenIPC/decoder/issues) -- Documentation: [openipc.org](https://openipc.org) - -[![Telegram](https://openipc.org/images/telegram_button.svg)](https://openipc.org/our-channels) - [logo]: https://raw.githubusercontent.com/OpenIPC/.github/refs/heads/main/assets/openipc-logo-black.svg diff --git a/README.ru.md b/README.ru.md index a10fba29..53b3363e 100644 --- a/README.ru.md +++ b/README.ru.md @@ -11,38 +11,49 @@ Компактное приложение для Android: показывает RTSP-потоки с IP-камер и декодирует H.264/H.265 аппаратно. Создано для камер с прошивкой [OpenIPC](https://openipc.org). Работает на телефонах, планшетах, телевизорах, ТВ-приставках и автомобильных устройствах. +- **Видео:** H.264/H.265 через MediaCodec, MJPEG по RTSP и HTTP; автоматическое восстановление декодера +- **Звук:** AAC, Opus, G.711, L16 +- **Режимы:** одна камера или мультивид — до 8 камер и камера, которая сама шлёт RTP; декодирование подстраивается под возможности устройства +- **Сеть:** RTSP поверх TCP, UDP (Auto переходит на TCP) или multicast, RTP, который камера шлёт сама (Majestic `outgoing`), авторизация Basic и Digest, автоматическое переподключение +- **Прочее:** пульт Android TV, масштаб, веб-интерфейс камеры, адреса хранятся в зашифрованном виде, отладочная страница на порту 8085 +

Мультивид: восемь камер и меню

+

+ Одиночный режим сразу после подключения: транспорт, кодеки и битрейт +

+ +

+ Меню на телефоне + WebUI на телефоне: страница прокручивает поле над клавиатурой +

+ +

+ Отладочная страница: снимок с текстом каждой ячейки
+ /stats.json + /log +

+ ## Скачать Готовые сборки лежат в [релизе **latest**](https://github.com/OpenIPC/decoder/releases/tag/latest): -| Файл | Что это | -|------------------|-----------------------------------| +| Файл | Что это | +|---|---| | `decoder-v2.apk` | **Текущая версия**, рекомендуется | -| `decoder.apk` | Старая версия из ветки `basic` | +| `decoder.apk` | Старая версия из ветки `basic` | -У каждой версии 2.xx есть и свой релиз — [`v2.01`](https://github.com/OpenIPC/decoder/releases/tag/v2.01) и далее, на [странице релизов](https://github.com/OpenIPC/decoder/releases) — с APK этой версии и списком изменений после предыдущей. +У каждой версии 2.xx есть и свой релиз на [странице релизов](https://github.com/OpenIPC/decoder/releases) — с APK этой версии и списком изменений после предыдущей. Есть бета-канал в [Google Play](https://play.google.com/apps/internaltest/4701231141824898735). Чтобы попасть в него, пришлите свой e-mail через [Telegram-каналы OpenIPC](https://openipc.org/our-channels). -## Возможности - -- **Видео:** воспроизведение RTSP, аппаратное декодирование H.264/H.265 через MediaCodec, MJPEG по RTSP (RTP/JPEG) и по HTTP (`multipart/x-mixed-replace`) в одиночном режиме и в мультивиде, автоматическое восстановление декодера, поток UDP с потерями показывается с замаскированными повреждениями вместо чёрного экрана, предупреждение, если видеодекодер устройства перестал отвечать, картинка вписывается в экран любой формы -- **Звук:** AAC (RFC 3640 `mpeg4-generic`, ADTS, MP4A-LATM), Opus, G.711 μ-law/A-law, L16 PCM (моно и стерео); в мультивиде — звук выбранной ячейки -- **Режимы:** одна камера или мультивид (Quad) — до 8 камер и ячейка U (камера, которая сама шлёт RTP) сеткой 2×2 или 3×3; адаптивное декодирование (все кадры или только ключевые для каждой ячейки, предел определяется на устройстве); S: только ключевые кадры — и в мультивиде, и в одиночном режиме -- **Управление:** масштаб двумя пальцами, перемещение, сброс двойным касанием, пульт Android TV (Leanback) -- **Сеть:** RTSP поверх TCP или UDP (Auto, режим по умолчанию, переходит на TCP, если UDP отклонён, видео не приходит или ключевые кадры теряются), фактический транспорт показывается при подключении, RTSP multicast, приём RTP, который камера сама шлёт по UDP (unicast или multicast), авторизация Basic и Digest, RTSP keepalive, автоматическое переподключение с растущей задержкой, индикатор качества связи -- **WebUI:** встроенный браузер для веб-интерфейса камеры -- **Прочее:** 8 слотов для камер, адреса хранятся в зашифрованном виде, скриншот долгим нажатием, приём ссылок из других приложений - ## Требования - Android 5.0 (API 21) или новее -- IP-камера с RTSP или камера, которая сама отправляет RTP по UDP (OpenIPC Majestic `outgoing`), см. [Транспорты](#транспорты) -- Сервисы Google (GAPPS) **не** нужны: приложение работает и на AOSP-сборках без них. WebUI использует системный WebView, поэтому ему нужен рабочий пакет WebView (на некоторых устройствах это *Android System WebView* от Google); его отключение затрагивает только WebUI — [#15](https://github.com/OpenIPC/decoder/issues/15) +- IP-камера с RTSP или HTTP MJPEG либо камера, которая сама отправляет RTP по UDP (OpenIPC Majestic `outgoing`) +- Сервисы Google (GAPPS) **не** нужны. Для WebUI нужен рабочий системный WebView ([#15](https://github.com/OpenIPC/decoder/issues/15)) ## Быстрый старт @@ -52,125 +63,27 @@ ``` rtsp://username:password@ip_address:554/stream=0 ``` - Под адресом можно выбрать [транспорт](#транспорты); у нового слота по умолчанию Auto (UDP с откатом на TCP). + Под адресом можно выбрать транспорт; у нового слота по умолчанию Auto (UDP с откатом на TCP). Другие виды адресов — в [Transports](docs/transports.md) (на английском). 4. Нажмите **Save**. Приложение сразу подключится и само переподключится, если поток прервётся. ## Управление -### Меню - -Меню — это сетка: восемь слотов сверху, ниже два ряда по четыре кнопки. - ``` [ 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 ] [ Settings | WebUI | Grid | Slides ] [ Exit | Reset | Debug | UDP ] ``` -

- Меню на телефоне - WebUI на телефоне: страница прокручивает поле над клавиатурой -

- -| Пункт | Действие | -|--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| **1**–**8** | Переключиться на слот камеры. Цвет номера активного слота показывает качество связи: зелёный — джиттер меньше 100 мс, жёлтый — 100–300 мс, красный — больше 300 мс. Серый номер — пустой слот. | -| **Settings** | Изменить адрес и транспорт активного слота (только в режиме одной камеры; в остальных — серая) | -| **WebUI** | Открыть веб-интерфейс камеры; логин и пароль берутся из RTSP-адреса (только в режиме одной камеры; в остальных — серая) | -| **Grid** | Вид: мультивид (2×2 или 3×3) или одна камера. Grid не меняет режим воспроизведения (Slides) | -| **Slides** | Режим воспроизведения: все кадры или только ключевые (картинка обновляется раз в интервал ключевых кадров, MJPEG — раз в секунду), минимальная нагрузка на декодер. Работает и в мультивиде, и в одиночном режиме, вид не меняет | -| **Exit** | Закрыть приложение. Мелкий текст под ним — версия приложения и хеш сборки. | -| **Reset** | Очистить все настройки после подтверждения (фокус на **Cancel**), чтобы настроить приложение с чистого листа: удаляются слоты камер, транспорты и остальные настройки, приложение открывается как сразу после установки. Сами камеры не меняются | -| **Debug** | Текст в углу каждой ячейки мультивида (разрешение, кодек, декодировано/получено кадр/с, ключевые кадры, состояние) или ни в одной | -| **UDP** | Ячейка U на весь экран: камера, которая сама шлёт RTP по UDP на порт 5600 | - -Включённые пункты — Grid, Slides, Debug, UDP и активный слот — залиты оранжевым; пункт, на котором стоит пульт, обведён голубой рамкой. - -### Жесты и кнопки - -| Ввод | Действие | -|------------------------------|-------------------------------------------------------------------------| -| Касание / **OK** / **Enter** | Открыть или закрыть меню | -| **Back** / **Esc** | Закрыть меню | -| Щипок, затем перетаскивание | Увеличение (до 5×) и перемещение, в режиме одной камеры | -| Двойное касание | Сбросить масштаб | -| Долгое нажатие | Одна камера: скриншот в `Pictures/OpenIPC`. Мультивид: звук этой ячейки | - -### Открытие камеры из другого приложения - -Приложение принимает текст через «Поделиться» (`ACTION_SEND`, `text/plain`). Если поделиться ссылкой `rtsp://`, `udp://` или `http://` из другого приложения, например из OpenIPC Network, она запишется в активный слот. Необязательный параметр `transport` (`tcp`, `udp`, `auto`, `multicast`) задаёт транспорт слота: - -```sh -adb shell am start -a android.intent.action.SEND -t text/plain \ - --es android.intent.extra.TEXT 'rtsp://user:pass@192.168.1.10/stream=0' \ - --es transport udp -n com.openipc.decoder/.Decoder -``` - -### Мультивид (Quad) - -Показывает настроенные слоты сеткой 2×2, а если занят хотя бы один из слотов 5–8 — сеткой 3×3. Девятая ячейка сетки 3×3, ячейка **U**, показывает камеру, которая сама отправляет RTP по UDP на порт 5600 устройства (`udp://@:5600`, см. [слоты udp://](#слоты-udp-majestic-outgoing)); пока поток не пришёл, в ней перечислены все адреса устройства, куда его слать. Кнопка **UDP** в меню открывает её на весь экран. Другие порты и multicast-группы указываются в слоте адресом `udp://`. У каждой ячейки своё подключение и свой декодер, а у видеодекодера устройства постоянный бюджет: тестовая приставка AG-216, например, декодирует в полном темпе примерно два потока 1080p30. Поэтому сетка подстраивается: ячейки стартуют на ключевых кадрах, затем по приоритету (ячейка U, пока в неё идёт поток, ячейка со звуком, потом по номеру слота) переходят на все кадры, пока декодер успевает. Остальные остаются на ключевых кадрах — картинка обновляется раз в интервал между ключевыми кадрами (в тексте в углу — `key frames`, меню **Debug**). Предел определяется замером на каждом устройстве. Дополнительные потоки камер (substream), если они есть, позволяют показать в полном темпе больше ячеек. Ячейка на ключевых кадрах сверх лимита экземпляров аппаратного декодера (у AG-216 — 4 × H.265) работает на программном декодере (`· software` в углу); только сверх и этого бюджета она показывает `Decoder limit: N × H.265`. Если не справляются даже ключевые кадры, ячейка с наименьшим приоритетом ставится на паузу (`Paused: decoder overloaded`) и после спокойного периода пробуется снова; потери в первые секунды после запуска сетки, когда все декодеры стартуют разом, не учитываются. Ячейка, у которой поток UDP теряет кусок каждого ключевого кадра, показывает кадры с замаскированными повреждениями (`· damaged`), а не чёрный экран. Некоторые камеры допускают только одно RTSP-подключение одновременно. Долгое нажатие на ячейку включает её звук (повторное — выключает); у выбранной ячейки виден значок AUDIO, выбор запоминается. Коснитесь любой ячейки, чтобы открыть меню. - -## Транспорты - -У каждого слота свой транспорт: он выбирается под адресом в **Settings** (или параметром `transport` в присланной ссылке). - -| Транспорт | Как приходит видео | Примечание | -|------------------------|-------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------| -| **TCP** | RTP внутри RTSP-соединения | Работает везде, в том числе через NAT и межсетевые экраны | -| **UDP** | RTP по UDP, пара портов на дорожку | Меньше задержка в локальной сети. Потерянные пакеты пропускаются, а не запрашиваются заново. На TCP не переходит | -| **Auto** (по умолчанию) | UDP, а если UDP не работает — TCP | Переходит на TCP, если камера отказала в UDP или видео не пришло за 3 с, и остаётся на TCP, пока слот не изменят | -| **Multicast** | RTSP multicast (Android 8.0+), при отказе — TCP | Для серверов с RTSP multicast. OpenIPC Majestic и камеры XM его не поддерживают: отвечают 400 или 451, и слот играет по TCP в том же соединении | - -Фактический транспорт показывается при запуске потока в одиночном режиме (`Connected • UDP • H.265 …`), на отладочной странице и в `/stats.json` (`transport`; настройка слота — `transport_mode`). После отката там будет, например, `TCP (auto)`. - -

- Одиночный режим сразу после подключения: транспорт, кодеки и битрейт -

- -### Слоты UDP (Majestic outgoing) - -Слот может принимать и RTP, который камера отправляет сама, без RTSP. Для этого вместо `rtsp://` укажите адрес `udp://`: - -| Адрес | Значение | -|-------------------------------------|-----------------------------------------------------------------------------| -| `udp://@:5600` | Слушать порт 5600 | -| `udp://@239.1.2.3:5600` | Войти в multicast-группу 239.1.2.3 на порту 5600 | -| `udp://192.168.1.10@239.1.2.3:5600` | То же, но принимать пакеты только от этого отправителя | -| `udp://@:5600?codec=h265&pt=97` | Задать кодек или тип нагрузки видео, если значения по умолчанию не подходят | - -Кодек определяется по типу нагрузки RTP (96 → H.264, 97 → H.265), если не задан `codec=`; смену кодека на камере приложение подхватывает на лету. Только видео. Два слота не могут слушать один и тот же порт. - -На камере добавьте устройство в адресаты `outgoing` в `/etc/majestic.yaml` (затем перезапустите Majestic): - -```yaml -outgoing: - servers: - - udp://192.168.1.20:5600 -``` - -`/api/v1/outgoing.json` на камере показывает, идёт ли отправка. Majestic умеет слать и в multicast-группу (`udp://239.1.2.3:5600`), тогда одну камеру могут смотреть несколько устройств. В Wi‑Fi приложение держит multicast lock, пока состоит в группе. - -### Слоты HTTP MJPEG - -Слот принимает и адрес HTTP MJPEG, например у Majestic `http://user:password@<камера>/mjpeg`: поток `multipart/x-mixed-replace`, по одному JPEG в каждой части, с chunked-кодированием или без. Учётные данные Basic отправляются сразу, Digest — после запроса камеры; поток переподключается так же, как RTSP. Работает в одиночном режиме и в мультивиде, транспорт показывается как `HTTP`. Звука в этом режиме нет. - -## Отладочная страница (порт 8085) +| Действие | Результат | +|---|---| +| Касание / **OK** | Открыть или закрыть меню | +| **1**–**8** | Слот камеры; цвет номера показывает качество связи | +| **Grid** / **Slides** | Мультивид или одна камера / все кадры или только ключевые | +| **Debug** / **UDP** | Текст в ячейках мультивида / камера, которая шлёт RTP на порт 5600 | +| Два пальца, двойное касание | Масштаб и перемещение, сброс масштаба (одиночный режим) | +| Долгое нажатие | Скриншот (одиночный режим) / звук этой ячейки (мультивид) | -Пока приложение работает, оно отдаёт отладочную информацию по HTTP на порту **8085** — для браузера или `curl` из той же сети: - -- `http://<приставка>:8085/` — снимок на всю страницу, поверх каждой ячейки камера, размер, fps и потери (красным); в шапке версия, время приставки и аптайм. Переключатель **Info On / Off** в шапке убирает текст поверх ячеек (и список под ними на узком экране), остаётся только картинка; по умолчанию **Off**, выбор запоминается в браузере. Красная пометка в шапке сообщает, что видеодекодер устройства перестал отвечать (`decoder_stuck`); -- `http://<приставка>:8085/snapshot.jpg` (`?labels=0` — только кадры) — картинка с раскладкой экрана в разрешении экрана (не больше 1920×1080) из кадров, которые приложение показывает сейчас; ячейка без видео — чёрная с её статусом. Снимается только по запросу; страница обновляет её раз в 3, 5 (по умолчанию) или 10 с, пока вкладка видна; -- `http://<приставка>:8085/stats.json` — то же в JSON, для скриптов; -- В одиночном виде страница и снимок всё равно показывают всю сетку (#100): камера на экране обведена синей рамкой, а каждый другой настроенный слот, пока страница открыта, примерно раз в 45 с получает короткую проверку потока (подключение, несколько секунд пакетов, без декодирования); в его ячейке — результат, например `Stream OK · H.265 · 25 fps · 3.1 Mbit/s · checked 12 s ago`, или ошибка. Слот с камерой, которая на экране, не проверяется (некоторые камеры принимают только одного клиента), как и слоты `udp://`. В `stats.json` эти ячейки — в поле `slots`; -- `http://<приставка>:8085/log` — последние 500 строк лога приложения. - -Отвечает только клиентам из локальной сети (loopback, 10/8, 172.16/12, 192.168/16, link-local, IPv6 fc00::/7), остальным — 403. Логины и пароли не показываются. - -

- Отладочная страница: снимок с текстом каждой ячейки
- /stats.json - /log -

+Все пункты меню, пульт, передача адреса из другого приложения и **Reset** — в [Usage](docs/usage.md) (на английском). ## Известные ограничения @@ -179,140 +92,41 @@ outgoing: > > - **RTSP multicast требует Android 8.0+** и сервера, который его поддерживает; камеры OpenIPC его не поддерживают — [#69](https://github.com/OpenIPC/decoder/issues/69) > - **Не поддерживаются звук G.726 и MP4A-LATM с конфигурацией внутри потока (`cpresent=1`).** OpenIPC Majestic их не использует, а камеры с G.726 обычно умеют отдавать G.711 или AAC — [#14](https://github.com/OpenIPC/decoder/issues/14) -> - **MJPEG декодируется программно на устройствах без рабочего JPEG-декодера** (AG-216 его объявляет, но создать не может); аппаратное декодирование используется, если устройство предоставляет `video/jpeg` — [#132](https://github.com/OpenIPC/decoder/issues/132) -> - **Видеодекодер AG-216 может перестать отвечать до перезагрузки устройства**: кадры приходят, но ни один не декодируется; приложение сообщает об этом на экране и на отладочной странице, причина пока неизвестна — [#145](https://github.com/OpenIPC/decoder/issues/145) -> - **Переключение между мультивидом и одиночным режимом может на несколько секунд заморозить картинку** на некоторых ТВ-приставках (замечено на AG-216) — [#76](https://github.com/OpenIPC/decoder/issues/76) +> - **MJPEG декодируется программно на устройствах без рабочего JPEG-декодера** (AG-216 его объявляет, но создать не может) — [#132](https://github.com/OpenIPC/decoder/issues/132) +> - **Видеодекодер AG-216 может перестать отвечать до перезагрузки устройства**: кадры приходят, но ни один не декодируется; приложение сообщает об этом на экране и на отладочной странице — [#145](https://github.com/OpenIPC/decoder/issues/145) > - **Логин и пароль передаются в открытом виде** по RTSP/HTTP без шифрования (Basic, если камера не запросила Digest); RTSPS не поддерживается и не планируется — [#16](https://github.com/OpenIPC/decoder/issues/16) +> - **Скриншоты и снимок отладочной страницы требуют Android 7.0+** > > Известные ошибки собраны отдельно, под [меткой `bug`](https://github.com/OpenIPC/decoder/issues?q=is%3Aissue+is%3Aopen+label%3Abug). -## Ветки - -| Ветка | Назначение | -|---------|-----------------------------------------------------------------------------------------------------------------------| -| `v2` | **Основная ветка разработки** (по умолчанию). Модульный код в `android/`. Сборки публикуются как `decoder-v2.apk`. | -| `basic` | Старая версия 1.0x в одном файле, сохранена для справки и для старых установок. Сборки публикуются как `decoder.apk`. | +## Документация -Pull request делайте от ветки `v2`. +Документация написана на английском языке; ссылки ведут на английские документы. -## Сборка +| Документ | Содержание | +|---|---| +| [Usage](docs/usage.md) | меню, жесты и клавиши, пульт, мультивид, Slides, ячейка U, WebUI, передача адреса из другого приложения, скриншоты, Reset | +| [Transports](docs/transports.md) | форматы адресов и параметры: RTSP поверх TCP/UDP/Auto/multicast, приём `udp://` (Majestic `outgoing`), HTTP MJPEG; кодеки, авторизация, что умеют серверы | +| [Debug](docs/debug.md) | отладочная страница на порту 8085, описание полей `/stats.json`, `/log`, снимки, что приложить к сообщению об ошибке | +| [Architecture](docs/architecture.md) | путь данных, классы, потоки, планировщик декодирования, вывод через SurfaceView, структура репозитория | +| [Testing and releases](docs/testing.md) | ветки, CI, юнит-тесты, версии и релизы, сборки веток, `mapping.txt`, локальная сборка | +| [Security](docs/security.md) | хранилище учётных данных, отладочный сервер (только локальная сеть), скрытие паролей в логах, отсутствие шифрования в сети | +| [Devices](docs/devices.md) | проверенные и неработающие устройства, особенности аппаратных декодеров, отчёты об устройствах и камерах | +| [Drones and Wi‑Fi toys](docs/toys.md) | как передают видео игрушки, FLOW-UFO (в тестировании), что попробовать с неизвестной игрушкой, что прислать | +| [Laboratory](LABORATORY.md) | тестовый стенд: устройства, камеры, доступ, методики | -Нужны JDK 17 и Android SDK (platform 36). Путь к SDK задаётся через `ANDROID_HOME` или `android/local.properties`. +История изменений — в описаниях релизов на [странице релизов](https://github.com/OpenIPC/decoder/releases) (CI пишет их для каждой версии v2.xx). -```bash -cd android -./gradlew assembleDebug # отладочная сборка, ключи подписи не нужны -# → app/build/outputs/apk/debug/app-debug.apk -``` - -Для релизной сборки нужно хранилище ключей `android/key.jks` и переменные окружения: +## Поддержка -```bash -export KEYSTORE_PASS=your_password -export KEY_ALIAS=your_alias -export KEY_PASS=your_key_password -./gradlew assembleRelease # → app/build/outputs/apk/release/app-release.apk -``` +- Вопросы и сообщения об ошибках: [GitHub Issues](https://github.com/OpenIPC/decoder/issues) +- Проверили приложение на устройстве или камере? Пришлите [отчёт об устройстве](https://github.com/OpenIPC/decoder/issues/new?template=device_report.yml) или [отчёт о камере](https://github.com/OpenIPC/decoder/issues/new?template=camera_report.yml) +- OpenIPC: [openipc.org](https://openipc.org) -Отладочная и релизная сборки подписаны разными ключами. Чтобы перейти с одной на другую, сначала удалите приложение; сохранённые адреса камер удалятся вместе с ним. - -**CI:** GitHub Actions собирает каждый push. Push в `v2` и `basic` обновляет релиз [latest](https://github.com/OpenIPC/decoder/releases/tag/latest) и отправляет уведомление в Telegram-канал OpenIPC. Для остальных веток APK прикладывается только как артефакт на странице запуска. - -## Архитектура - -| Класс | Назначение | -|------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------| -| `Decoder` | Главная activity: интерфейс, жизненный цикл, настройки, декодирование видео одной камеры, сетка мультивида | -| `RtspClient` | Поток одной камеры: RTSP-сессия, сборка NAL, звук, качество связи | -| `RtspSession` | Одна RTSP-сессия поверх TCP, UDP или multicast: рукопожатие, приём RTP, keepalive, сторожевой таймер, переподключение с растущей задержкой | -| `RtspAuth`, `Sdp` | Авторизация Basic и Digest; разбор описания сессии камеры | -| `Transport`, `UdpReceiver`, `RtpReorder` | Согласование транспорта; приём по UDP и multicast с фильтром отправителя; восстановление порядка пакетов | -| `RtpListener` | Слоты `udp://`: RTP, который камера отправляет сама | -| `HttpMjpeg` | Слоты `http://`: MJPEG по HTTP (`multipart/x-mixed-replace`, chunked или нет) | -| `NalAssembler` | Сборка NAL-блоков H.264/H.265 из RTP-пакетов, отбрасывание блоков, повреждённых потерями | -| `JpegAssembler` | MJPEG: сборка целых JPEG-кадров из RTP/JPEG (RFC 2435) | -| `JpegCodec`, `JpegRenderer` | MJPEG на экране: JPEG-декодер устройства, если он есть, иначе BitmapFactory | -| `DecodePlanner` | Мультивид: какие ячейки декодируют все кадры, а какие только ключевые, в пределах выученной ёмкости декодера | -| `MediaCodecManager`, `DecoderRetry` | Жизненный цикл аппаратного видеодекодера и восстановление после ошибок | -| `AudioManager`, `AudioConfig` | Звук: преобразование G.711/L16, декодирование AAC/Opus, вывод через AudioTrack | -| `QuadCell` | Одна ячейка мультивида: своя сессия, декодер и звук по выбору | -| `MenuManager` | Всплывающее меню, редактор адреса, браузер WebUI | -| `CredentialStore`, `KeystoreCipher` | Зашифрованное хранение адресов камер (Android Keystore) | -| `FramePool`, `Frame` | Пул буферов, снижает нагрузку на сборщик мусора | -| `App`, `Crash` | Обработчик необработанных исключений и экран отчёта о сбое | -| `DebugServer`, `NetInfo`, `Snapshots` | Отладочная страница на порту 8085 (только локальная сеть), адреса устройства, снимки экрана | - -Сеть, декодирование видео и звук работают в отдельных потоках, поэтому медленный декодер не блокирует сокет. - -## Безопасность - -- Логин и пароль берутся из адреса и передаются в заголовке `Authorization`: сначала Basic, Digest — если камера его запросит. В строку RTSP-запроса они не попадают. -- RTSP, RTP по UDP и HTTP без шифрования, RTSPS не поддерживается. Если камера ждёт логин и пароль прямо в пути или параметрах адреса (например, `user=…_password=…`), они уходят с каждым запросом. Используйте приложение только в доверенных сетях, см. [#16](https://github.com/OpenIPC/decoder/issues/16). -- Адреса камер вместе с паролями хранятся в закрытом хранилище приложения в зашифрованном виде (AES-256-GCM, ключ в Android Keystore, Android 6.0+). На Android 5.x или если Keystore недоступен — в открытом виде. Резервное копирование отключено (`allowBackup="false"`). -- Отладочная страница на порту 8085 без шифрования и пароля; она отвечает только клиентам из локальной сети и не показывает логины и пароли. - -## Совместимость с устройствами - -Проверили приложение на своём устройстве? [**Отправьте Device report**](https://github.com/OpenIPC/decoder/issues/new?template=device_report.yml) — это займёт минуту и поможет другим пользователям. Проверили камеру? [**Отправьте Camera report**](https://github.com/OpenIPC/decoder/issues/new?template=camera_report.yml): производитель, модель, адрес RTSP (без пароля), кодеки и что работает. - -### Проверенные устройства - -Большинство отчётов ниже получено для старой версии 1.0x (ветка `basic`); в столбце *Версия* указано, какая версия проверялась. В новых отчётах указывайте версию приложения. - -| Тип | Устройство | Android | Ядро | Версия | Примечание | -|--------------|--------------------------------------|------------------------------------|----------------|---------|----------------------------------------------------------------------------------------------------| -| Телефон | Asus ZC553KL | 8.1 | 3.18.71 | 1.0x | | -| Телефон | Blackview BV4900Pro | 12 | 4.19.191 | 1.0x | | -| Телефон | Oppo 5X | ColorOS 15.0 | 5.15.149 | 1.0x | | -| Телефон | Oppo A17 | ColorOS 12.1 | 4.19.191 | 1.0x | | -| Телефон | Samsung Galaxy A51 | 13 | 4.14.113 | 1.0x | | -| Телефон | Samsung Galaxy M21 (SM-M215F/DSN) | 12 | 4.14.113 | 1.0x | | -| Телефон | Samsung Galaxy S25 | 15 | 6.6.30 | 1.0x | | -| Телефон | Samsung A55 5G | 14 | 6.1.93 | 1.0x | | -| Телефон | Samsung Galaxy J7 | 10 | 3.18.150 | 1.0x | | -| Телефон | Samsung S8 | 9 | 4.4.153 | 1.0x | | -| Телефон | Samsung S23 Ultra | 15 | 5.15.153 | 1.0x | | -| Телефон | TECNO KM4 | 15 | 5.15.194 | v2 2.04 | Сенсорное устройство лаборатории: WebUI с экранной клавиатурой проверен ([#159](https://github.com/OpenIPC/decoder/issues/159), [#160](https://github.com/OpenIPC/decoder/issues/160)); Close под строкой состояния ([#169](https://github.com/OpenIPC/decoder/issues/169)) | -| Телефон | Xiaomi Redmi 7A | 10 | 4.9.261 | 1.0x | | -| Телефон | Xiaomi Redmi Note 7 (M1901F7G) | 10 | 4.4.192 | 1.0x | | -| Телефон | Xiaomi Redmi Note 13 4G | 13 | 5.15.94 | 1.0x | | -| Планшет | Lenovo TB-8504X | 8.1.0 | 3.18.71 | v2 2.00 | H.265 и мультивью работают; ввод в WebUI исправлен в 2.02 ([#159](https://github.com/OpenIPC/decoder/issues/159)) | -| Планшет | Lenovo TB-X304L | 8.1.0 | 3.18.71 | 1.0x | Изображение обрезано в 1.0x, исправлено в v2 ([#17](https://github.com/OpenIPC/decoder/issues/17)) | -| Планшет | Lenovo TB-X606F | 10 | — | 1.0x | Изображение обрезано в 1.0x, исправлено в v2 ([#17](https://github.com/OpenIPC/decoder/issues/17)) | -| Планшет | Lenovo Tab P11 | 11 | 4.19.157-perf+ | 1.0x | Изображение обрезано в 1.0x, исправлено в v2 ([#17](https://github.com/OpenIPC/decoder/issues/17)) | -| Планшет | Samsung Galaxy Tab A | 7.1.1 | 3.10.49 | 1.0x | | -| Планшет | Samsung Galaxy Tab A | 11 | 4.9.227 | 1.0x | | -| Планшет | Samsung Tab A7 (SM-T505) | 12 | 4.19.157-perf- | 1.0x | | -| ТВ-приставка | A95X F3 AIR | 9 (SlimBOXtv AOSP 9.14, без GAPPS) | 4.9.113 | 1.0x | | -| ТВ-приставка | IE X3 Air | 9 (SlimBOXtv AOSP 9.14, без GAPPS) | 4.9.113 | 1.0x | | -| ТВ-приставка | AG-216 | 10 | 4.9.170 | v2 2.00 | Тестовое устройство v2; бюджет декодера по замеру ≈ 1080p60 суммарно (два 1080p30 в полном темпе) | -| ТВ-приставка | SberBox SBDV-00001 | своя | 4.9.228 | 1.0x | | -| ТВ-приставка | X96Q (Allwinner H313) | 10 | 4.9.170 | 1.0x | | -| Проектор | SIBIRLINK HY300 / HY320 | 11 | 4.19.323 | v2 2.02 | 1080p H.264 / H.265 работает; 2304×1296 и 2560×1440 не запускаются ([#165](https://github.com/OpenIPC/decoder/issues/165), [#166](https://github.com/OpenIPC/decoder/issues/166)) | -| Телевизор | Haier Candy Android TV 2K | 11 | 4.9.243+ | 1.0x | Нет значка в списке приложений | -| Телевизор | Yandex TV Novex NVX-55U169TSY | 7.1.1 | 4.4.3 | 1.0x | | -| Авто | Медиацентр JCAC10003 (ac8227/ac8229) | 12 | 3.18.79+ | 1.0x | | -| Авто | Зеркало Z55 | 8.1.0 | 4.4.83 | 1.0x | | - -### Не работает - -| Тип | Устройство | Android | Ядро | Версия | Примечание | -|--------------|---------------------------|---------|---------|--------|------------| -| ТВ-приставка | ТВ-приставка без названия | 5.1.1 | 3.14.29 | 1.0x | Нет видео | - -## История версий - -Изменения описаны на [странице релизов](https://github.com/OpenIPC/decoder/releases) и в [истории коммитов](https://github.com/OpenIPC/decoder/commits/v2). История версий 1.0x — в [README ветки `basic`](https://github.com/OpenIPC/decoder/tree/basic#version-history). +[![Telegram](https://openipc.org/images/telegram_button.svg)](https://openipc.org/our-channels) ## Лицензия [The Prosperity Public License 3.0.0](LICENSE.md): бесплатно для некоммерческого использования; для коммерческого — пробный период 30 дней. Полный текст — в [LICENSE.md](LICENSE.md). -## Поддержка - -- Вопросы и сообщения об ошибках: [GitHub Issues](https://github.com/OpenIPC/decoder/issues) -- Документация: [openipc.org](https://openipc.org) - -[![Telegram](https://openipc.org/images/telegram_button.svg)](https://openipc.org/our-channels) - [logo]: https://raw.githubusercontent.com/OpenIPC/.github/refs/heads/main/assets/openipc-logo-black.svg diff --git a/README.zh-CN.md b/README.zh-CN.md index 32179cf6..734692d5 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -11,38 +11,49 @@ 一款小巧的 Android 应用,用于观看 IP 摄像机的 RTSP 视频流,支持 H.264/H.265 硬件解码。专为运行 [OpenIPC](https://openipc.org) 固件的摄像机设计,可在手机、平板、电视、电视盒子和车载设备上使用。 +- **视频:** 通过 MediaCodec 解码 H.264/H.265,支持 RTSP 和 HTTP 上的 MJPEG;解码器出错后自动恢复 +- **音频:** AAC、Opus、G.711、L16 +- **显示:** 单路画面,或多画面模式:最多 8 路摄像机外加一路主动推送 RTP 的摄像机,解码方式根据设备能力自动调整 +- **网络:** RTSP over TCP、UDP(Auto 模式会回退到 TCP)或组播,接收摄像机主动推送的 RTP(Majestic `outgoing`),支持 Basic 和 Digest 认证,断线自动重连 +- **其他:** 支持 Android TV 遥控器、缩放、摄像机网页界面,地址加密保存,端口 8085 上的调试页面 +

多画面模式:八路摄像机与菜单

+

+ 单画面刚连接时:传输方式、编码和码率 +

+ +

+ 手机上的菜单 + 手机上的 WebUI:页面会把输入框滚动到键盘上方 +

+ +

+ 调试页面:快照及每个画面的文字
+ /stats.json + /log +

+ ## 下载 可直接安装的版本位于 [**latest** 发布页](https://github.com/OpenIPC/decoder/releases/tag/latest): -| 文件 | 说明 | -|------------------|------------------------| +| 文件 | 说明 | +|---|---| | `decoder-v2.apk` | **当前版本**,推荐使用 | -| `decoder.apk` | `basic` 分支的旧版本 | +| `decoder.apk` | `basic` 分支的旧版本 | -每个 2.xx 版本另有独立的发布 — [`v2.01`](https://github.com/OpenIPC/decoder/releases/tag/v2.01) 及之后的版本,见[发布页面](https://github.com/OpenIPC/decoder/releases) — 包含该版本的 APK 以及相对上一版本的更改。 +每个 2.xx 版本在[发布页面](https://github.com/OpenIPC/decoder/releases)上另有独立的发布,包含该版本的 APK 以及相对上一版本的更改。 [Google Play](https://play.google.com/apps/internaltest/4701231141824898735) 上提供测试渠道。如需加入,请通过 [OpenIPC Telegram 频道](https://openipc.org/our-channels)发送您的电子邮箱。 -## 功能 - -- **视频:** RTSP 播放,通过 MediaCodec 进行 H.264/H.265 硬件解码,支持 RTSP 上的 MJPEG(RTP/JPEG)和 HTTP 上的 MJPEG(`multipart/x-mixed-replace`),单画面和多画面均可,解码器出错后自动恢复,有丢包的 UDP 流以掩盖损坏的画面显示而不是黑屏,设备视频解码器停止响应时给出提示,画面适配任意屏幕比例 -- **音频:** AAC(RFC 3640 `mpeg4-generic`、ADTS、MP4A-LATM)、Opus、G.711 μ-law/A-law、L16 PCM(单声道/立体声);多画面模式下播放所选画面的声音 -- **显示:** 单路画面,或多画面模式(Quad),以 2×2 或 3×3 网格最多显示 8 路摄像机外加 U 画面(主动推送 RTP 的摄像机);自适应解码(每个画面全帧率或仅关键帧,上限按设备实测);S:仅解码关键帧,多画面和单画面均适用 -- **操作:** 双指缩放、拖动平移、双击复位,支持 Android TV 遥控器(Leanback 启动器) -- **网络:** RTSP over TCP 或 UDP(默认 Auto:UDP 被拒绝、无视频或关键帧持续丢失时回退到 TCP),连接时显示实际使用的传输方式、RTSP 组播、接收摄像机通过 UDP 主动推送的 RTP(单播或组播),支持 Basic 和 Digest 认证、RTSP 保活,断线后自动重连(指数退避),连接质量指示 -- **WebUI:** 内置浏览器,可打开摄像机的网页管理界面 -- **其他:** 8 个摄像机槽位,地址加密保存,长按截图,可接收其他应用分享的地址 - ## 系统要求 - Android 5.0(API 21)或更高版本 -- 支持 RTSP 的 IP 摄像机,或能通过 UDP 主动推送 RTP 的摄像机(OpenIPC Majestic `outgoing`),参见[传输方式](#传输方式) -- **不**需要 Google 服务(GAPPS):应用可在没有 GAPPS 的 AOSP 系统上运行。WebUI 使用系统 WebView,因此需要可用的 WebView 软件包(部分设备上为 Google 的 *Android System WebView*);禁用它只影响 WebUI — [#15](https://github.com/OpenIPC/decoder/issues/15) +- 支持 RTSP 或 HTTP MJPEG 的 IP 摄像机,或能通过 UDP 主动推送 RTP 的摄像机(OpenIPC Majestic `outgoing`) +- **不**需要 Google 服务(GAPPS)。WebUI 需要可用的系统 WebView([#15](https://github.com/OpenIPC/decoder/issues/15)) ## 快速上手 @@ -52,125 +63,27 @@ ``` rtsp://username:password@ip_address:554/stream=0 ``` - 地址下方可选择[传输方式](#传输方式),新槽位默认为 Auto(优先 UDP,必要时回退到 TCP)。 + 地址下方可选择传输方式,新槽位默认为 Auto(优先 UDP,必要时回退到 TCP)。其他地址格式见 [Transports](docs/transports.md)(英文)。 4. 点击 **Save**。应用会立即连接;视频流中断时会自动重连。 ## 操作说明 -### 菜单 - -菜单是一个网格:上方为八个槽位,下方两行各四个按钮。 - ``` [ 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 ] [ Settings | WebUI | Grid | Slides ] [ Exit | Reset | Debug | UDP ] ``` -

- 手机上的菜单 - 手机上的 WebUI:页面会把输入框滚动到键盘上方 -

- -| 菜单项 | 功能 | -|--------------|-----------------------------------------------------------------------------------------------------------------------------------------------| -| **1**–**8** | 切换摄像机槽位。当前槽位编号的颜色表示连接质量:绿色为抖动低于 100 毫秒,黄色为 100–300 毫秒,红色为高于 300 毫秒。灰色编号表示该槽位未配置。 | -| **Settings** | 编辑当前槽位的地址和传输方式(仅单路模式;其他模式下为灰色) | -| **WebUI** | 打开摄像机网页界面,用户名和密码取自 RTSP 地址(仅单路模式;其他模式下为灰色) | -| **Grid** | 视图:多画面(2×2 或 3×3)或单路画面。Grid 不改变播放模式(Slides) | -| **Slides** | 播放模式:全部帧或仅关键帧(画面按关键帧间隔更新,MJPEG 每秒一帧),解码器负载最低。多画面和单画面均适用,不切换视图 | -| **Exit** | 退出应用。下方的小字是应用版本号和构建哈希。 | -| **Reset** | 确认后清除所有设置(默认焦点在 **Cancel**),以便从头重新配置:删除摄像机槽位、传输方式和其他设置,应用像刚安装后一样打开。摄像机本身不受影响 | -| **Debug** | 在每个多画面格子的角落显示文字(分辨率、编码、已解码/已接收帧率、关键帧、状态),或全部不显示 | -| **UDP** | 全屏显示 U 画面:通过 UDP 向 5600 端口推送 RTP 的摄像机 | - -已开启的项目(Grid、Slides、Debug、UDP 和当前槽位)以橙色填充;遥控器焦点所在的项目带浅蓝色边框。 - -### 手势与按键 - -| 输入 | 功能 | -|---------------------------|---------------------------------------------------------------------| -| 点击 / **OK** / **Enter** | 打开或关闭菜单 | -| **Back** / **Esc** | 关闭菜单 | -| 双指捏合后拖动 | 缩放(最大 5 倍)并平移,仅单路画面 | -| 双击 | 恢复原始大小 | -| 长按 | 单路画面:截图并保存到 `Pictures/OpenIPC`。多画面:播放该画面的声音 | - -### 从其他应用打开摄像机 - -应用可接收纯文本分享(`ACTION_SEND`,`text/plain`)。从其他应用(例如 OpenIPC Network)分享 `rtsp://`、`udp://` 或 `http://` 地址后,该地址会写入当前槽位。可选参数 `transport`(`tcp`、`udp`、`auto`、`multicast`)用于设置该槽位的传输方式: - -```sh -adb shell am start -a android.intent.action.SEND -t text/plain \ - --es android.intent.extra.TEXT 'rtsp://user:pass@192.168.1.10/stream=0' \ - --es transport udp -n com.openipc.decoder/.Decoder -``` - -### 多画面模式(Quad) - -以 2×2 网格显示已配置的槽位;只要使用了第 5–8 个槽位中的任意一个,就改为 3×3 网格。3×3 网格的第九格即 **U** 画面,播放通过 UDP 向本设备 5600 端口主动推送 RTP 的摄像机(`udp://@:5600`,参见 [udp:// 槽位](#udp-槽位-majestic-outgoing));收到流之前,其中列出本设备可用于推流的所有地址。菜单中的 **UDP** 可将其全屏显示。其他端口或组播组请在槽位中填写 `udp://` 地址。每个画面都有独立的连接和解码器,而设备视频解码器的处理能力是固定的:例如测试用的 AG-216 盒子以全帧率大约只能解码两路 1080p30。因此网格会自适应:各画面先只解码关键帧,然后按优先级(正在接收流的 U 画面,其次是正在播放声音的画面,再按槽位顺序)在解码器跟得上时切换为全帧率。其余画面保持只解码关键帧,画面按关键帧间隔更新(角落文字显示 `key frames`,菜单 **Debug**)。上限在每台设备上通过实测确定。如摄像机提供子码流,可让更多画面以全帧率播放。超出硬件解码器实例上限(AG-216 为 4 × H.265)的仅关键帧画面改用软件解码器(角落显示 `· software`);只有连这一预算也超出时才显示 `Decoder limit: N × H.265`。如果连关键帧都处理不过来,优先级最低的画面会被暂停(`Paused: decoder overloaded`),平稳一段时间后再重试;网格启动后最初几秒(所有解码器同时启动)的丢帧不计入。若某画面的 UDP 流每个关键帧都丢失一部分,会显示掩盖了损坏的画面(`· damaged`),而不是黑屏。部分摄像机同一时间只允许一个 RTSP 客户端。长按某个画面即可播放其声音(再次长按则静音);被选中的画面显示 AUDIO 标记,选择会被记住。点击任意画面即可打开菜单。 - -## 传输方式 - -每个槽位可单独设置传输方式:在 **Settings** 中地址下方选择(或通过分享链接的 `transport` 参数设置)。 - -| 传输方式 | 视频如何到达 | 说明 | -|-----------------|---------------------------------------------|-------------------------------------------------------------------------------------------------------------------------| -| **TCP** | RTP 封装在 RTSP 连接中 | 适用于所有环境,也可穿过 NAT 和防火墙 | -| **UDP** | 通过 UDP 传输 RTP,每路一对端口 | 局域网内延迟更低。丢失的包直接跳过,不会重传。不会回退到 TCP | -| **Auto**(默认) | 优先 UDP,UDP 不可用时改用 TCP | 摄像机拒绝 UDP 或 3 秒内未收到视频时回退到 TCP,并保持 TCP 直到修改该槽位 | -| **Multicast** | RTSP 组播(Android 8.0+),被拒绝时改用 TCP | 适用于提供 RTSP 组播的服务器。OpenIPC Majestic 和 XM 摄像机不支持:它们返回 400 或 451,槽位会在同一连接中改用 TCP 播放 | - -实际使用的传输方式会在单画面模式开始播放时显示(`Connected • UDP • H.265 …`),也会显示在调试页面和 `/stats.json` 中(`transport`;槽位的设置为 `transport_mode`)。回退后会显示为例如 `TCP (auto)`。 - -

- 单画面刚连接时:传输方式、编码和码率 -

- -### UDP 槽位 (Majestic outgoing) - -槽位也可以接收摄像机不经 RTSP 主动推送的 RTP。为此请填写 `udp://` 地址代替 `rtsp://`: - -| 地址 | 含义 | -|-------------------------------------|----------------------------------------| -| `udp://@:5600` | 监听 5600 端口 | -| `udp://@239.1.2.3:5600` | 加入 239.1.2.3 组播组,端口 5600 | -| `udp://192.168.1.10@239.1.2.3:5600` | 同上,但只接收该发送端的数据包 | -| `udp://@:5600?codec=h265&pt=97` | 默认值不适用时,指定编码或视频负载类型 | - -除非指定 `codec=`,编码根据 RTP 负载类型判断(96 → H.264,97 → H.265);播放过程中摄像机切换编码也会自动跟随。仅支持视频。两个槽位不能监听同一端口。 - -在摄像机上,将本设备添加为 `outgoing` 的目标(编辑 `/etc/majestic.yaml` 后重启 Majestic): - -```yaml -outgoing: - servers: - - udp://192.168.1.20:5600 -``` - -摄像机上的 `/api/v1/outgoing.json` 可查看推流是否正常。Majestic 也可以推送到组播组(`udp://239.1.2.3:5600`),这样多台设备可以同时观看同一台摄像机。在 Wi‑Fi 下,应用加入组播组期间会持有 multicast lock。 - -### HTTP MJPEG 槽位 - -槽位也可以填写 HTTP MJPEG 地址,例如 Majestic 的 `http://user:password@<摄像机>/mjpeg`:`multipart/x-mixed-replace` 流,每个部分是一帧 JPEG,可以是分块传输(chunked)。Basic 凭据随请求发送,摄像机要求时使用 Digest;断线后与 RTSP 一样自动重连。单画面和多画面均可播放,传输方式显示为 `HTTP`。此方式没有音频。 - -## 调试页面(端口 8085) +| 输入 | 功能 | +|---|---| +| 点击 / **OK** | 打开或关闭菜单 | +| **1**–**8** | 摄像机槽位;编号的颜色表示连接质量 | +| **Grid** / **Slides** | 多画面或单路画面 / 解码全部帧或仅关键帧 | +| **Debug** / **UDP** | 多画面中每个画面的文字 / 向端口 5600 推送 RTP 的摄像机 | +| 双指捏合、双击 | 缩放和平移、恢复原始大小(单路画面) | +| 长按 | 截图(单路画面)/ 播放该画面的声音(多画面) | -应用运行时会在 **8085** 端口通过 HTTP 提供调试信息,可在同一网络中用浏览器或 `curl` 访问: - -- `http://<设备>:8085/` — 快照铺满页面,每个画面上叠加摄像机、分辨率、帧率和丢帧(红色);页头显示版本、设备时间和运行时长。页头的 **Info On / Off** 开关可隐藏画面上的文字(以及窄屏时下方的列表),只保留画面;默认为 **Off**,选择会保存在浏览器中。设备视频解码器停止响应时,页头会出现红色提示(`decoder_stuck`); -- `http://<设备>:8085/snapshot.jpg`(`?labels=0`:仅画面)— 按屏幕布局拼成的图片,分辨率与屏幕相同(最大 1920×1080),取自应用当前显示的画面;没有视频的画面为黑色并显示其状态。仅在请求时截取;页面在标签页可见时每 3、5(默认)或 10 秒刷新一次; -- `http://<设备>:8085/stats.json` — 相同内容的 JSON,便于脚本使用; -- 单画面模式下,页面和快照仍显示完整网格(#100):屏幕上的摄像机用蓝框标出;页面打开期间,其他每个已配置的槽位约每 45 秒进行一次简短的流检测(连接、接收几秒数据包、不解码),其画面显示检测结果,例如 `Stream OK · H.265 · 25 fps · 3.1 Mbit/s · checked 12 s ago`,或错误信息。与屏幕上相同的摄像机的槽位不检测(部分摄像机只接受一个客户端),`udp://` 槽位也不检测。`stats.json` 中这些画面位于 `slots` 字段; -- `http://<设备>:8085/log` — 应用日志的最后 500 行。 - -仅响应本地网络中的客户端(loopback、10/8、172.16/12、192.168/16、链路本地地址、IPv6 fc00::/7),其他地址返回 403。不会显示用户名和密码。 - -

- 调试页面:快照及每个画面的文字
- /stats.json - /log -

+菜单各项、遥控器、从其他应用分享地址以及 **Reset** 的说明见 [Usage](docs/usage.md)(英文)。 ## 已知限制 @@ -179,140 +92,41 @@ outgoing: > > - **RTSP 组播需要 Android 8.0+**,且服务器须支持;OpenIPC 摄像机不支持 — [#69](https://github.com/OpenIPC/decoder/issues/69) > - **不支持 G.726 音频,也不支持流内配置的 MP4A-LATM(`cpresent=1`)。** OpenIPC Majestic 不使用这些格式,支持 G.726 的摄像机通常也能输出 G.711 或 AAC — [#14](https://github.com/OpenIPC/decoder/issues/14) -> - **在没有可用 JPEG 解码器的设备上 MJPEG 为软件解码**(AG-216 声明了 JPEG 解码器但无法创建);设备提供 `video/jpeg` 时使用硬件解码 — [#132](https://github.com/OpenIPC/decoder/issues/132) -> - **AG-216 的视频解码器可能停止响应,直到重启设备**:帧照常到达但无法解码;应用会在屏幕和调试页面上提示,原因尚不清楚 — [#145](https://github.com/OpenIPC/decoder/issues/145) -> - **在多画面与单画面之间切换时,部分电视盒子(如 AG-216)的画面可能会冻结数秒** — [#76](https://github.com/OpenIPC/decoder/issues/76) +> - **在没有可用 JPEG 解码器的设备上 MJPEG 为软件解码**(AG-216 声明了 JPEG 解码器但无法创建)— [#132](https://github.com/OpenIPC/decoder/issues/132) +> - **AG-216 的视频解码器可能停止响应,直到重启设备**:帧照常到达但无法解码;应用会在屏幕和调试页面上提示 — [#145](https://github.com/OpenIPC/decoder/issues/145) > - **用户名和密码以明文传输**(未加密的 RTSP/HTTP;摄像机未要求 Digest 时发送 Basic),不支持 RTSPS,也不计划支持 — [#16](https://github.com/OpenIPC/decoder/issues/16) +> - **截图和调试页面快照需要 Android 7.0+** > > 已知的程序错误另行收录在 [`bug` 标签](https://github.com/OpenIPC/decoder/issues?q=is%3Aissue+is%3Aopen+label%3Abug)下。 -## 分支 - -| 分支 | 说明 | -|---------|--------------------------------------------------------------------------------------| -| `v2` | **主开发分支**(默认)。模块化代码位于 `android/`,构建产物发布为 `decoder-v2.apk`。 | -| `basic` | 旧版 1.0x,单文件实现,保留作参考并供旧环境使用。构建产物发布为 `decoder.apk`。 | +## 文档 -请基于 `v2` 分支提交 pull request。 +文档以英文撰写,以下链接均指向英文文档。 -## 构建 +| 文档 | 内容 | +|---|---| +| [Usage](docs/usage.md) | 菜单、手势与按键、遥控器、多画面、Slides、U 画面、WebUI、从其他应用分享地址、截图、Reset | +| [Transports](docs/transports.md) | 地址格式与参数:RTSP over TCP/UDP/Auto/组播、`udp://` 推流接收(Majestic `outgoing`)、HTTP MJPEG;编码、认证、各服务器的支持情况 | +| [Debug](docs/debug.md) | 端口 8085 上的调试页面、`/stats.json` 字段说明、`/log`、快照、提交错误报告时需附带的内容 | +| [Architecture](docs/architecture.md) | 数据流程、类、线程、解码调度器、SurfaceView 渲染、仓库结构 | +| [Testing and releases](docs/testing.md) | 分支、CI、单元测试、版本与发布、分支构建、`mapping.txt`、本地构建 | +| [Security](docs/security.md) | 凭据存储、调试服务器(仅限局域网)、日志脱敏、网络传输未加密 | +| [Devices](docs/devices.md) | 已验证和无法使用的设备、硬件解码器说明、设备与摄像机报告 | +| [Drones and Wi‑Fi toys](docs/toys.md) | 玩具如何传输视频、FLOW-UFO(测试中)、遇到未知玩具时可以尝试什么、需要提交哪些信息 | +| [Laboratory](LABORATORY.md) | 测试平台:设备、摄像机、访问方式、测试方法 | -需要 JDK 17 和 Android SDK(platform 36)。通过 `ANDROID_HOME` 或 `android/local.properties` 指定 SDK 路径。 +更新历史见[发布页面](https://github.com/OpenIPC/decoder/releases)上的发布说明(CI 会为每个 v2.xx 版本自动生成)。 -```bash -cd android -./gradlew assembleDebug # 调试版本,无需签名密钥 -# → app/build/outputs/apk/debug/app-debug.apk -``` - -发布版本需要密钥库 `android/key.jks` 以及以下环境变量: +## 支持 -```bash -export KEYSTORE_PASS=your_password -export KEY_ALIAS=your_alias -export KEY_PASS=your_key_password -./gradlew assembleRelease # → app/build/outputs/apk/release/app-release.apk -``` +- 问题与错误报告:[GitHub Issues](https://github.com/OpenIPC/decoder/issues) +- 在设备或摄像机上测试过本应用?欢迎提交[设备报告](https://github.com/OpenIPC/decoder/issues/new?template=device_report.yml)或[摄像机报告](https://github.com/OpenIPC/decoder/issues/new?template=camera_report.yml) +- OpenIPC:[openipc.org](https://openipc.org) -调试版本和发布版本使用不同的密钥签名。在两者之间切换前需先卸载应用,已保存的摄像机地址会一并删除。 - -**CI:** GitHub Actions 会构建每一次 push。推送到 `v2` 和 `basic` 时会更新 [latest](https://github.com/OpenIPC/decoder/releases/tag/latest) 发布页并通知 OpenIPC Telegram 频道。其他分支的 APK 只作为工作流产物附在运行页面上。 - -## 架构 - -| 类 | 作用 | -|------------------------------------------|---------------------------------------------------------------------------| -| `Decoder` | 主 Activity:界面、生命周期、设置、单路视频解码、多画面网格 | -| `RtspClient` | 单路数据流:RTSP 会话、NAL 重组、音频、连接质量 | -| `RtspSession` | 单个 RTSP 会话(TCP、UDP 或组播):握手、RTP 接收、保活、看门狗、退避重连 | -| `RtspAuth`、`Sdp` | Basic 与 Digest 认证;解析摄像机的会话描述 | -| `Transport`、`UdpReceiver`、`RtpReorder` | 传输方式协商;带发送端过滤的 UDP 与组播接收;数据包重排序 | -| `RtpListener` | `udp://` 槽位:接收摄像机主动推送的 RTP | -| `HttpMjpeg` | `http://` 槽位:HTTP 上的 MJPEG(`multipart/x-mixed-replace`) | -| `NalAssembler` | 将 RTP 包重组为 H.264/H.265 NAL 单元,丢弃因丢包而损坏的单元 | -| `JpegAssembler` | MJPEG:由 RTP/JPEG(RFC 2435)重组完整的 JPEG 帧 | -| `JpegCodec`、`JpegRenderer` | MJPEG 显示:有设备 JPEG 解码器时使用它,否则使用 BitmapFactory | -| `DecodePlanner` | 多画面:在解码器实测能力内决定哪些画面全帧率、哪些仅解码关键帧 | -| `MediaCodecManager`、`DecoderRetry` | 硬件视频解码器的生命周期管理与出错后的恢复 | -| `AudioManager`、`AudioConfig` | 音频处理:G.711/L16 转换、AAC/Opus 解码、通过 AudioTrack 播放 | -| `QuadCell` | 多画面中的单个画面:独立的会话、解码器和可选音频 | -| `MenuManager` | 弹出菜单、地址编辑器、WebUI 浏览器 | -| `CredentialStore`、`KeystoreCipher` | 摄像机地址加密存储(Android Keystore) | -| `FramePool`、`Frame` | 缓冲区池,减轻垃圾回收压力 | -| `App`、`Crash` | 未捕获异常处理与崩溃报告界面 | -| `DebugServer`、`NetInfo`、`Snapshots` | 8085 端口调试页面(仅本地网络)、设备地址、屏幕快照 | - -网络、视频解码和音频分别运行在独立线程中,解码器较慢时也不会阻塞网络读取。 - -## 安全说明 - -- 用户名和密码从地址中提取,通过 `Authorization` 头发送:默认使用 Basic,摄像机要求时改用 Digest。不会出现在 RTSP 请求行中。 -- RTSP、UDP 上的 RTP 和 HTTP 均未加密,不支持 RTSPS。若摄像机要求将用户名和密码写在地址路径或参数中(例如 `user=…_password=…`),它们会随每个请求发送。请仅在可信网络中使用,参见 [#16](https://github.com/OpenIPC/decoder/issues/16)。 -- 摄像机地址(含密码)加密保存在应用私有存储中(AES-256-GCM,密钥保存在 Android Keystore,Android 6.0 及以上),在 Android 5.x 或 Keystore 不可用时以明文保存,并已禁用备份(`allowBackup="false"`)。 -- 8085 端口上的调试页面未加密、无密码;仅响应本地网络客户端,且不会显示用户名和密码。 - -## 设备兼容性 - -在您的设备上测试过本应用?请[**提交 Device report**](https://github.com/OpenIPC/decoder/issues/new?template=device_report.yml),只需一分钟,可以帮助其他用户。 测试过某款摄像机?请[**提交 Camera report**](https://github.com/OpenIPC/decoder/issues/new?template=camera_report.yml):厂商、型号、RTSP 地址(去掉密码)、编码以及哪些功能正常。 - -### 已验证设备 - -以下大多数设备的报告基于旧版 1.0x(`basic` 分支);“版本”一列为实际测试的版本。新报告请注明应用版本。 - -| 类型 | 设备 | Android | 内核 | 版本 | 备注 | -|----------|---------------------------------------|------------------------------------|----------------|---------|-----------------------------------------------------------------------------------------| -| 手机 | Asus ZC553KL | 8.1 | 3.18.71 | 1.0x | | -| 手机 | Blackview BV4900Pro | 12 | 4.19.191 | 1.0x | | -| 手机 | Oppo 5X | ColorOS 15.0 | 5.15.149 | 1.0x | | -| 手机 | Oppo A17 | ColorOS 12.1 | 4.19.191 | 1.0x | | -| 手机 | Samsung Galaxy A51 | 13 | 4.14.113 | 1.0x | | -| 手机 | Samsung Galaxy M21 (SM-M215F/DSN) | 12 | 4.14.113 | 1.0x | | -| 手机 | Samsung Galaxy S25 | 15 | 6.6.30 | 1.0x | | -| 手机 | Samsung A55 5G | 14 | 6.1.93 | 1.0x | | -| 手机 | Samsung Galaxy J7 | 10 | 3.18.150 | 1.0x | | -| 手机 | Samsung S8 | 9 | 4.4.153 | 1.0x | | -| 手机 | Samsung S23 Ultra | 15 | 5.15.153 | 1.0x | | -| 手机 | TECNO KM4 | 15 | 5.15.194 | v2 2.04 | 实验室触屏设备:已验证 WebUI 与屏幕键盘([#159](https://github.com/OpenIPC/decoder/issues/159)、[#160](https://github.com/OpenIPC/decoder/issues/160));Close 按钮被状态栏遮挡([#169](https://github.com/OpenIPC/decoder/issues/169)) | -| 手机 | Xiaomi Redmi 7A | 10 | 4.9.261 | 1.0x | | -| 手机 | Xiaomi Redmi Note 7 (M1901F7G) | 10 | 4.4.192 | 1.0x | | -| 手机 | Xiaomi Redmi Note 13 4G | 13 | 5.15.94 | 1.0x | | -| 平板 | Lenovo TB-8504X | 8.1.0 | 3.18.71 | v2 2.00 | H.265 与多画面正常;WebUI 输入问题已在 2.02 修复([#159](https://github.com/OpenIPC/decoder/issues/159)) | -| 平板 | Lenovo TB-X304L | 8.1.0 | 3.18.71 | 1.0x | 1.0x 中画面边缘被裁切,v2 已修复([#17](https://github.com/OpenIPC/decoder/issues/17)) | -| 平板 | Lenovo TB-X606F | 10 | — | 1.0x | 1.0x 中画面边缘被裁切,v2 已修复([#17](https://github.com/OpenIPC/decoder/issues/17)) | -| 平板 | Lenovo Tab P11 | 11 | 4.19.157-perf+ | 1.0x | 1.0x 中画面边缘被裁切,v2 已修复([#17](https://github.com/OpenIPC/decoder/issues/17)) | -| 平板 | Samsung Galaxy Tab A | 7.1.1 | 3.10.49 | 1.0x | | -| 平板 | Samsung Galaxy Tab A | 11 | 4.9.227 | 1.0x | | -| 平板 | Samsung Tab A7 (SM-T505) | 12 | 4.19.157-perf- | 1.0x | | -| 电视盒子 | A95X F3 AIR | 9(SlimBOXtv AOSP 9.14,无 GAPPS) | 4.9.113 | 1.0x | | -| 电视盒子 | IE X3 Air | 9(SlimBOXtv AOSP 9.14,无 GAPPS) | 4.9.113 | 1.0x | | -| 电视盒子 | AG-216 | 10 | 4.9.170 | v2 2.00 | v2 测试设备;实测解码能力约为总计 1080p60(全帧率两路 1080p30) | -| 电视盒子 | SberBox SBDV-00001 | 定制系统 | 4.9.228 | 1.0x | | -| 电视盒子 | X96Q(全志 H313) | 10 | 4.9.170 | 1.0x | | -| 投影仪 | SIBIRLINK HY300 / HY320 | 11 | 4.19.323 | v2 2.02 | 1080p H.264 / H.265 正常;2304×1296 和 2560×1440 无法播放([#165](https://github.com/OpenIPC/decoder/issues/165)、[#166](https://github.com/OpenIPC/decoder/issues/166)) | -| 电视 | Haier Candy Android TV 2K | 11 | 4.9.243+ | 1.0x | 应用列表中没有图标 | -| 电视 | Yandex TV Novex NVX-55U169TSY | 7.1.1 | 4.4.3 | 1.0x | | -| 车载 | 多媒体主机 JCAC10003(ac8227/ac8229) | 12 | 3.18.79+ | 1.0x | | -| 车载 | 后视镜 Z55 | 8.1.0 | 4.4.83 | 1.0x | | - -### 无法使用 - -| 类型 | 设备 | Android | 内核 | 版本 | 备注 | -|----------|------------------|---------|---------|------|--------| -| 电视盒子 | 未知型号电视盒子 | 5.1.1 | 3.14.29 | 1.0x | 无视频 | - -## 版本历史 - -更新内容请参见[发布页](https://github.com/OpenIPC/decoder/releases)和[提交历史](https://github.com/OpenIPC/decoder/commits/v2)。1.0x 版本的历史记录见 [`basic` 分支的 README](https://github.com/OpenIPC/decoder/tree/basic#version-history)。 +[![Telegram](https://openipc.org/images/telegram_button.svg)](https://openipc.org/our-channels) ## 许可证 [The Prosperity Public License 3.0.0](LICENSE.md):非商业用途可免费使用;商业用途可试用 30 天。完整条款见 [LICENSE.md](LICENSE.md)。 -## 支持 - -- 问题与错误报告:[GitHub Issues](https://github.com/OpenIPC/decoder/issues) -- 文档:[openipc.org](https://openipc.org) - -[![Telegram](https://openipc.org/images/telegram_button.svg)](https://openipc.org/our-channels) - [logo]: https://raw.githubusercontent.com/OpenIPC/.github/refs/heads/main/assets/openipc-logo-black.svg diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 00000000..0a25f96e --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,149 @@ +# Architecture + +- [Data path](#data-path) +- [Classes](#classes) +- [Threads](#threads) +- [Decode planner](#decode-planner) +- [Rendering: SurfaceView](#rendering-surfaceview) +- [Settings and state](#settings-and-state) +- [Repository layout](#repository-layout) + +## Data path + +``` + layer rtsp:// udp:// rtp:// http:// + ───────────────────────────────────────────────────────────────────────────────── + network RtspSession RtpListener HttpMjpeg + RtspAuth, Sdp, Transport, pushed RTP, multipart MJPEG, + UdpReceiver, RtpReorder video only video only + │ │ │ │ + assembly NalAssembler JpegAssembler NalAssembler │ whole JPEGs + H.264/H.265 RTP/JPEG H.264/H.265 │ + │ │ │ │ + │ └────────────────┼──────────┐ │ + ├────────────────────────────────┘ │ │ + ▼ ▼ ▼ + queue bounded frame queue per stream, buffers from FramePool + │ │ + ▼ ▼ + decode MediaCodecManager JpegRenderer + MediaCodec H.264/H.265 JpegCodec or BitmapFactory + │ │ + ▼ ▼ + screen SurfaceView: the single view (Decoder) or one per multi-view cell (QuadCell) + + audio RtspSession → AudioConfig / AudioManager → MediaCodec (AAC, Opus) + or conversion (G.711, L16) → AudioTrack; one stream's sound at a time + beside DecodePlanner (multi-view, once a second), DebugServer on port 8085 +``` + +The single view runs one `RtspClient` (one `RtspSession`, assemblers, audio, connection quality) feeding the activity's decoder. Multi-view runs one `QuadCell` per cell, each with its own session, assemblers, queue and decoder. `udp://` and `http://` slots use the same `RtspSession` front, which hands their packets or frames to the same callbacks. + +## Classes + +| Class | Role | +|---|---| +| `Decoder` | Main activity: UI, lifecycle, settings, single-view decoding, multi-view grid, debug state | +| `RtspClient` | Single-view stream: RTSP session, NAL assembly, audio, connection quality (jitter) | +| `RtspSession` | One session over TCP, UDP or multicast (and the `udp://`, `http://` fronts): handshake, RTP receive, keepalive, watchdog, reconnect with back-off, Auto fallback | +| `RtspAuth`, `Sdp` | Basic and Digest authentication; parsing of the session description | +| `Transport` | Slot transport mode; the RTSP `Transport` header (request and reply) | +| `UdpReceiver`, `RtpReorder` | UDP and multicast receive with a sender filter; packet reordering | +| `RtpListener` | `udp://` slots: plain RTP pushed by the camera | +| `HttpMjpeg` | `http://` slots: MJPEG over HTTP (`multipart/x-mixed-replace`, chunked or not) | +| `NalAssembler` | H.264/H.265 NAL units from RTP; drops units damaged by packet loss, or passes them on cut short | +| `JpegAssembler` | Whole JPEG frames from RTP/JPEG (RFC 2435) | +| `JpegCodec`, `JpegRenderer` | MJPEG on screen: the device's JPEG decoder where there is one, otherwise BitmapFactory | +| `MediaCodecManager`, `DecoderRetry` | Video decoder lifecycle, instance budget per MIME type, software fallback, recovery after errors | +| `CodecInfo` | The device's video decoders and their declared limits (for `/stats.json`) | +| `DecodePlanner` | Multi-view: which cells decode every frame and which only key frames | +| `AudioManager`, `AudioConfig` | Audio: G.711/L16 conversion, AAC/Opus decoding, AudioTrack playback | +| `QuadCell` | One multi-view cell: its own session, decoder, queue and optional audio | +| `MenuManager` | Menu grid, URL dialog, reset dialog, WebUI browser | +| `CredentialStore`, `KeystoreCipher` | Encrypted storage of camera URLs (Android Keystore) | +| `FramePool`, `Frame` | Buffer pool that reduces garbage-collector load | +| `DebugServer`, `LogBuffer`, `NetInfo`, `Snapshots` | Debug server on port 8085, redacted log ring, device addresses and the LAN rule, screen snapshots | +| `StreamProbe`, `StreamProbes` | Short stream checks of the slots not on screen (single view, debug page in use) | +| `App`, `Crash`, `Restart` | Uncaught-exception handler and crash screen; restart after Reset | + +## Threads + +Network, video decoding and audio run on separate threads, so a slow decoder never blocks the socket. + +| Thread | Work | +|---|---| +| UI (main) | layout, menu, planner step and stats once a second, debug state on request | +| `-net` | one per session: connect, RTSP, receive RTP or HTTP, assemble, queue | +| `-wd` | one per session: watchdog (no video for 8 s → reconnect) and RTSP keepalive | +| single-view pool (2) | video decode from the queue; audio playback | +| `Quad-N-dec` | one per multi-view cell: decode from the cell's queue | +| `quad-audio` | audio of the selected multi-view cell | +| `rtsp-aac-decode` | AAC/Opus output of the audio decoder | +| `decoder-release` | short-lived: stops and releases a decoder off the UI thread | +| `debug-http-*` | accept loop, two workers at background priority, request deadlines | +| `probe-sched`, `probe` | stream checks: scheduler and two workers at background priority | +| `codec-info` | lists the device's decoders once at start | + +The queues between network and decoder are bounded (30 frames per cell); when a queue is full, the frame is dropped and counted as a decoder loss (`lost_fps`), not a network loss. + +## Decode planner + +`DecodePlanner` ([#85](https://github.com/OpenIPC/decoder/issues/85), [#189](https://github.com/OpenIPC/decoder/issues/189)) decides once a second which multi-view cells decode every frame and which only key frames, so that every camera stays on screen while the hardware decoder keeps up. Pure logic, unit-tested without Android. + +**Load** is counted in macroblocks per second (16×16 blocks × frames per second), the unit decoders declare their throughput in. A cell's load is its full-rate load or its key-frame load; paused cells and cells on a software decoder do not count. + +**Capacity** starts as the value learnt on this device before, or the decoder's declared budget, or 1080p60 when none is declared. It is never below one 1080p30 stream (the floor): losses below that come from a stalled system, not from the decoder. + +**Priority:** the U cell while it receives a stream, the cell whose sound plays, then slot order. + +| Step | Rule | +|---|---| +| warm-up | the first 10 s after the grid starts: losses decide nothing, promotions that fit go on | +| settle | after a change, 3 s before the next one | +| loss signal | decoder losses (queue full, no input buffer): at full rate ≥ 2 frames/s and ≥ 10 % of the received rate; on key frames any loss. 3 lossy samples within 10 s trigger a demotion | +| demote | the lowest-priority full-rate cell goes to key frames. The capacity drops to 90 % of the load, never below a proven load; that load is remembered as failed, and the next probe waits twice as long (up to 10 min) | +| transient | a loss below a load that already played quietly is forgiven once: the cell is demoted, but the capacity and the failed load are kept. Forgiven again only after a load at least as high plays quietly again | +| pause | when even key frames are too much, the lowest-priority cell is paused (never the last one playing); it is resumed on key frames after a quiet period that doubles with every pause | +| promote | the highest-priority key-frame cell whose load is known, if the result fits within the capacity and stays below 90 % of a failed load. Beyond the capacity only after a quiet period (a probe, from 30 s). A lower cell may go first only if it surely fits | +| proven | a load that plays 30 s without losses is proven; above the capacity it raises it. A failure at about that load is forgotten | +| retry | a failed load is tried again one hour after the last demotion | +| unknown | a playing cell whose size and rate are not known yet holds the lower cells for up to 10 s | +| stall | a second in which nothing decodes at all (and nothing is lost) is skipped and does not count as quiet | + +The learnt capacity and the failed load are stored per device (`decoder_capacity_v2`, `decoder_failed_load_v2`); stored values at or below the floor are not trusted. `/stats.json` shows them under `decoder` ([debug.md](debug.md#top-level)). + +With **Slides** on, the planner is bypassed: every cell decodes key frames only. + +## Rendering: SurfaceView + +Both the single view and the multi-view cells render into `SurfaceView`s, not `TextureView`s. + +- Multi-view ([#61](https://github.com/OpenIPC/decoder/issues/61)): with a `TextureView` every decoded frame raises a frame-available callback that the framework delivers by attaching a native thread to the VM, about 1 MB/s of garbage with four streams. +- Single view ([#76](https://github.com/OpenIPC/decoder/issues/76), [#185](https://github.com/OpenIPC/decoder/pull/185)): the decoder renders to a layer of its own instead of the app's window buffer, whose `queueBuffer` stalled for seconds with every grid/single switch on Mali TV boxes. + +Consequences: + +- Zoom and pan lay the view out at the zoomed size and move it (a `SurfaceView` has no `setTransform()`, and its surface follows `setScaleX()` only from Android 7). +- Screenshots and `/snapshot.jpg` use `PixelCopy` (no `getBitmap()`), so they need Android 7.0+. +- MJPEG draws with `lockCanvas()`; switching back to a video decoder needs a fresh surface, so the view is hidden for a moment. + +## Settings and state + +`SharedPreferences` (`settings`): per slot `host_N` (the URL, sealed by `CredentialStore`) and `transport_N` (`TCP`, `UDP`, `AUTO`, `MULTICAST`; a slot without a value is Auto); the active slot and the view and modes (`active`, `quad_enabled`, `slides`, `show_stats`, `quad_audio`); the learnt decoder capacity. **Reset** clears it and restarts the app through the `Restart` activity in a separate process. See [security.md](security.md#credential-store). + +## Repository layout + +``` +android/ Gradle project + app/build.gradle versionName / versionCode, release signing, R8 + app/lint-baseline.xml known lint findings; only new ones fail CI + app/src/main/java/com/openipc/decoder/ the app (one package) + app/src/test/java/com/openipc/decoder/ JVM unit tests +.github/workflows/build.yml CI: tests, lint, signed builds, releases +.github/ISSUE_TEMPLATE/ device and camera report forms +docs/ these documents +screenshots/ README pictures +LABORATORY.md the test bench +``` + +The legacy 1.0x version lives in the `basic` branch (single file, built as `decoder.apk`). diff --git a/docs/debug.md b/docs/debug.md new file mode 100644 index 00000000..e31c2f48 --- /dev/null +++ b/docs/debug.md @@ -0,0 +1,203 @@ +# Debug + +- [Debug server](#debug-server) +- [Page](#page) +- [Snapshot](#snapshot) +- [Stream checks in the single view](#stream-checks-in-the-single-view) +- [stats.json fields](#statsjson-fields) +- [Log](#log) +- [Corner text in multi-view](#corner-text-in-multi-view) +- [Bug reports](#bug-reports) + +## Debug server + +While the app runs it serves its debug information over HTTP on port **8085**, for a browser or `curl` on the same network. It is always on and read-only. + +| Path | Content | +|---|---| +| `/` (or `/index.html`) | the [page](#page): snapshot with the text of each cell | +| `/stats.json` | the state as JSON, pretty-printed, see [fields](#statsjson-fields) | +| `/snapshot.jpg` | a picture of the screen, see [Snapshot](#snapshot); `?labels=0`: frames only | +| `/log` | the last 500 lines of the app's log, see [Log](#log) | + +- Only clients on the local network are answered; others get `403`. The rule and what is hidden: [security.md](security.md#debug-server). +- `GET` only (`405` otherwise), one request per connection, `Cache-Control: no-store`. +- Two worker threads at background priority, a short queue, a 20 s deadline per request: the server never gets in the way of decoding; a busy server drops the connection. +- If port 8085 is taken, the server retries every 10 s; `server` in `/stats.json` says why. +- On a phone that is only on a camera's or toy's Wi‑Fi, open the pages on the phone itself: `http://127.0.0.1:8085/`. + +The device's addresses are in `addresses` of `/stats.json`, and the [U cell](usage.md#the-u-cell) shows them while it waits for a stream. + +

+ Debug page: snapshot with each cell's text
+ /stats.json + /log +

+ +## Page + +`http://:8085/` + +- The snapshot fills the page, laid out like the screen, with each cell's slot, camera host, transport, size, fps and losses (in red) drawn over it. A decoded rate below 90 % of the received one is shown in orange. +- The header shows a status dot, the version and git hash, the device time and the uptime, and links to **JSON** and **Log**. +- **Info On / Off** hides the text over the cells (and the list below them on a narrow screen), leaving only the picture. It is **Off** until switched on; the choice is remembered in the browser. +- **Snapshot 3 s / 5 s / 10 s**: how often the picture is refreshed (default 5 s). `/stats.json` is fetched every 1.5 s. Both only while the tab is visible. +- On a screen narrower than 720 px the snapshot takes the full width and the cells are listed below it. + +| Status dot / header note | Meaning | +|---|---| +| green | the app answers | +| orange, `UI not responding within 1000 ms` | the UI thread is stuck (an ANR); only the device facts are reported | +| orange, `no activity` / `App in background` | no activity behind the server, or the app is paused | +| red, `video decoder not responding — restart the device` | `decoder_stuck`: frames arrive in several cells, none is decoded for 20 s ([#145](https://github.com/OpenIPC/decoder/issues/145)) | +| red, `offline` | the page cannot reach the app | + +## Snapshot + +`http://:8085/snapshot.jpg` + +A JPEG laid out like the screen, at the screen's resolution (at most 1920 wide, at least 640), made from the frames the app shows now. A cell without video is black with its status. With labels (the default) the text of each cell is drawn in; `?labels=0` gives the frames only, which is what the page uses. It is taken only when asked for and reused for 1 s, so several viewers cost one capture. `503` when there is nothing to show. Needs Android 7.0+ (PixelCopy). + +## Stream checks in the single view + +In the single view (and the U view) the page and the snapshot still show the whole grid ([#100](https://github.com/OpenIPC/decoder/issues/100)): the camera on screen is framed in blue, and every other configured slot gets a short stream check while the page is open. Its cell says what was found, for example `Stream OK · H.265 · 25 fps · 3.1 Mbit/s · checked 12 s ago`, or the error. + +- A check connects, receives packets for about 3 s and does not decode. +- Each slot is checked again about every 45 s, at most two checks run at a time, one new check starts every 2 s. +- Checks stop when no one has asked for the page for 60 s. +- Not checked: a slot of the same camera (host and port) as the one on screen, since some cameras take one client only; `udp://` slots (the port would be taken); the U cell. +- In `/stats.json` these cells are under `slots`, with `probe`. + +## stats.json fields + +`http://:8085/stats.json`. Logins and passwords are redacted in every string. Numbers that are not integers have two decimals; NaN and infinity are `null`. + +### Top level + +| Field | Type | Meaning | +|---|---|---| +| `app` | string | `OpenIPC Decoder` | +| `version` | string | versionName, e.g. `2.10` | +| `git` | string | short commit hash of the build | +| `time` | string | device time, `yyyy-MM-dd HH:mm:ss` | +| `uptime_s` | number | seconds since the device booted | +| `device` | string | manufacturer, model, SoC (Android 12+; else `Build.HARDWARE`) and board | +| `android` | string | release and SDK level, e.g. `10 (SDK 29)` | +| `screen` | string | display size in pixels, `WxH` | +| `addresses` | list | `http://:8085/` for every address of the device | +| `server` | string | `listening`, or why port 8085 could not be opened | +| `decoders` | list or `"loading"` | the device's video decoders, see [decoders](#decoders) | +| `ui_thread` | string | `ok`, or `not responding within 1000 ms` (then only the fields above are present) | +| `mode` | string | `single`, `multi-view`, `slides` (multi-view with Slides on), `udp` (U cell full-screen), `paused` (app in background); `no activity` when no activity is attached | +| `active_slot` | number | active slot, 1–8 | +| `decoder` | object | multi-view only: `capacity` (macroblocks/s), `learnt` (bool), `failed_load` (macroblocks/s, 0: none), see [architecture.md](architecture.md#decode-planner) | +| `decoder_stuck` | bool | multi-view only: frames arrive in at least two cells and none is decoded for 20 s | +| `udp` | object | U view only: `port` (5600) and `addresses` (the device's IPs to send to) | +| `single` | object | single view only, see [single](#single) | +| `cells` | list | multi-view and U view, see [cells](#cells) | +| `slots` | list | single view and U view: every grid cell with the stream checks, see [slots](#slots) | + +### decoders + +One entry per video decoder and MIME type (encoders are left out). + +| Field | Meaning | +|---|---| +| `name` | codec name as Android lists it | +| `type` | MIME type, e.g. `video/hevc` | +| `hardware` | hardware-accelerated (Android 10+) | +| `adaptive` | supports adaptive playback | +| `max_instances` | instances the codec declares (Android 6+) | +| `fps_1080p` | highest declared frame rate at 1920×1080, when 1080p is supported | + +### single + +| Field | Meaning | +|---|---| +| `url` | the slot URL, redacted | +| `transport` | in use, as in [cells](#cells) | +| `transport_mode` | the slot's setting: `TCP`, `UDP`, `AUTO`, `MULTICAST` | +| `video` | a frame was shown within the last 2 s | +| `status` | the status text on screen, or the session state | +| `size` | `WxH`, once known | +| `codec` | `H.264`, `H.265` or `MJPEG` | +| `audio` | audio codec: `NONE`, `AAC`, `LATM`, `OPUS`, `PCMU`, `PCMA`, `PCM` | +| `out_fps` | frames shown in the last second | +| `decode` | `full`, or `key frames` with Slides | + +### cells + +One entry per grid cell; `slot` is 1–8 or `U`. An empty cell has only `slot`, `video: false` and `text: "not configured"`. + +| Field | Meaning | +|---|---| +| `slot` | 1–8, or `U` | +| `video` | the cell showed a frame within the last 2 s | +| `text` | the corner text of the cell, see [below](#corner-text-in-multi-view) | +| `url` | the slot URL, redacted (`udp://@:5600` for U) | +| `transport` | in use: `TCP`, `UDP`, `multicast`, `HTTP`, `UDP push`, `multicast push`; after a fallback `TCP (auto)` or `TCP (multicast)`; the mode alone before anything plays | +| `transport_mode` | the slot's setting (`UDP` for U) | +| `out_fps` | frames decoded per second | +| `in_fps` | frames received per second | +| `lost_fps` | frames per second the decoder lost (queue full, no input buffer); network losses do not count | +| `nal_dropped` | NAL units dropped because one of their packets was lost | +| `nal_damaged` | slices passed on cut short at a lost piece; absent while damaged decoding is off | +| `decode` | `full` or `key frames` | +| `load` | macroblocks/s the cell costs in its current mode, once size and rate are known | +| `paused` | paused by the decode planner (`Paused: decoder overloaded`) | +| `audio` | this cell's sound plays | + +### slots + +Single view and U view: every cell of the grid as the debug page shows it (3×3 in the U view or when the active slot is 5–8). + +| Field | Meaning | +|---|---| +| `slot` | 1–8, or `U` | +| `on_screen` | this is the camera on screen | +| `url`, `transport_mode` | as in [cells](#cells), for configured slots only | +| `transport` | on screen: in use; otherwise what a check goes over (`check over TCP` for TCP and Auto slots, `check over UDP`, `check over multicast`, `UDP listen`, `HTTP`, `UDP push` for U) | +| `video`, `text` | on screen: as it plays; otherwise `false` and the check result or why there is none | +| `probe` | not on screen: the check, see below | + +| `probe` field | Meaning | +|---|---| +| `state` | `ok`, `error`, `checking`, `waiting`, `not checked` | +| `checked_s_ago` | seconds since the last check began | +| `summary` | result text | +| `codec`, `size` | when found | +| `fps`, `kbps`, `key_frames` | measured during the check | + +The FLOW-UFO test build ([toys.md](toys.md#flow-ufo)) adds a `flowufo` object per cell and in `single`; it is not in the releases. + +## Log + +`http://:8085/log`: the last 500 lines of the app's own log (logcat in `threadtime` format; without the `READ_LOGS` permission logcat returns only the app's own lines), redacted as they come in. The same lines over adb: `adb logcat -s OpenIPCDecoder`. Lines worth knowing are listed in [LABORATORY.md](../LABORATORY.md#log-lines-logcat--s-openipcdecoder). + +## Corner text in multi-view + +**Debug** in the menu shows it in every cell; the same text is `text` in `cells`. + +Examples: `1920×1080 H.265 25/25 fps` at full rate, `1920×1080 H.265 key frames · 0.5 fps` on key frames only. + +| Part | Meaning | +|---|---| +| `1920×1080 H.265` | stream size and codec (`H.264`, `H.265`, `MJPEG`) | +| `25/25 fps` | full rate: decoded / received frames per second | +| `key frames · 0.5 fps` | key frames only, with the frames shown per second (MJPEG: `reduced`, one frame per second) | +| `· software` | key frames on a software decoder, beyond the hardware instance limit (MJPEG: BitmapFactory) | +| `· damaged` | frames with lost pieces are decoded with the damage concealed | +| `Connecting…`, `Buffering…`, `Disconnected` | the session state before video arrives | +| `Decoder limit: N × H.265` | no decoder instance left, not even a software one | +| `Paused: decoder overloaded` | paused by the decode planner, tried again later | + +## Bug reports + +Send, in a [GitHub issue](https://github.com/OpenIPC/decoder/issues): + +1. App version and git hash (menu, under **Exit**), device model and Android version. +2. `/stats.json` and `/log` taken while the problem is visible (or after about 30 s of trying). +3. The camera: make, model, firmware, URL **without the password** (`user:***`), codec, resolution, transport. +4. A screenshot or `/snapshot.jpg` if the picture is wrong. + +The [camera report](https://github.com/OpenIPC/decoder/issues/new?template=camera_report.yml) and [device report](https://github.com/OpenIPC/decoder/issues/new?template=device_report.yml) forms ask for the same. Check that no password or public IP address is in what you post. diff --git a/docs/devices.md b/docs/devices.md new file mode 100644 index 00000000..6f36b6e6 --- /dev/null +++ b/docs/devices.md @@ -0,0 +1,69 @@ +# Devices + +- [Reporting](#reporting) +- [Verified devices](#verified-devices) +- [Not working](#not-working) +- [Hardware decoder notes](#hardware-decoder-notes) + +## Reporting + +Tested the app on your device? Please [**submit a Device report**](https://github.com/OpenIPC/decoder/issues/new?template=device_report.yml). It takes a minute and helps other users. Tested a camera? [**Submit a Camera report**](https://github.com/OpenIPC/decoder/issues/new?template=camera_report.yml): manufacturer, model, RTSP URL (without the password), codecs and what works. Reports are collected into the tables below. A `/stats.json` from the [debug page](debug.md) lists the device's decoders and their limits (`decoders`), which helps most with decoder problems. + +## Verified devices + +Most devices below were reported with the legacy 1.0x version (`basic` branch); the *App* column shows the version that was tested. New reports should include the app version. + +| Type | Device | Android | Kernel | App | Notes | +|-------------|----------------------------------------|-----------------------------------|----------------|---------|--------------------------------------------------------------------------------------------| +| Phone | Asus ZC553KL | 8.1 | 3.18.71 | 1.0x | | +| Phone | Blackview BV4900Pro | 12 | 4.19.191 | 1.0x | | +| Phone | Oppo 5X | ColorOS 15.0 | 5.15.149 | 1.0x | | +| Phone | Oppo A17 | ColorOS 12.1 | 4.19.191 | 1.0x | | +| Phone | Samsung Galaxy A51 | 13 | 4.14.113 | 1.0x | | +| Phone | Samsung Galaxy M21 (SM-M215F/DSN) | 12 | 4.14.113 | 1.0x | | +| Phone | Samsung Galaxy S25 | 15 | 6.6.30 | 1.0x | | +| Phone | Samsung A55 5G | 14 | 6.1.93 | 1.0x | | +| Phone | Samsung Galaxy J7 | 10 | 3.18.150 | 1.0x | | +| Phone | Samsung S8 | 9 | 4.4.153 | 1.0x | | +| Phone | Samsung S23 Ultra | 15 | 5.15.153 | 1.0x | | +| Phone | TECNO KM4 | 15 | 5.15.194 | v2 2.04 | Lab touch device: WebUI with the on-screen keyboard checked ([#159](https://github.com/OpenIPC/decoder/issues/159), [#160](https://github.com/OpenIPC/decoder/issues/160)); Close under the status bar ([#169](https://github.com/OpenIPC/decoder/issues/169)) | +| Phone | Xiaomi Redmi 7A | 10 | 4.9.261 | 1.0x | | +| Phone | Xiaomi Redmi Note 7 (M1901F7G) | 10 | 4.4.192 | 1.0x | | +| Phone | Xiaomi Redmi Note 13 4G | 13 | 5.15.94 | 1.0x | | +| Tablet | Lenovo TB-8504X | 8.1.0 | 3.18.71 | v2 2.00 | H.265 and multi-view work; WebUI input fixed in 2.02 ([#159](https://github.com/OpenIPC/decoder/issues/159)) | +| Tablet | Lenovo TB-X304L | 8.1.0 | 3.18.71 | 1.0x | Picture cropped in 1.0x, fixed in v2 ([#17](https://github.com/OpenIPC/decoder/issues/17)) | +| Tablet | Lenovo TB-X606F | 10 | — | 1.0x | Picture cropped in 1.0x, fixed in v2 ([#17](https://github.com/OpenIPC/decoder/issues/17)) | +| Tablet | Lenovo Tab P11 | 11 | 4.19.157-perf+ | 1.0x | Picture cropped in 1.0x, fixed in v2 ([#17](https://github.com/OpenIPC/decoder/issues/17)) | +| Tablet | Samsung Galaxy Tab A | 7.1.1 | 3.10.49 | 1.0x | | +| Tablet | Samsung Galaxy Tab A | 11 | 4.9.227 | 1.0x | | +| Tablet | Samsung Tab A7 (SM-T505) | 12 | 4.19.157-perf- | 1.0x | | +| Set-top box | A95X F3 AIR | 9 (SlimBOXtv AOSP 9.14, no GAPPS) | 4.9.113 | 1.0x | | +| Set-top box | IE X3 Air | 9 (SlimBOXtv AOSP 9.14, no GAPPS) | 4.9.113 | 1.0x | | +| Set-top box | AG-216 | 10 | 4.9.170 | v2 2.00 | v2 test device; decoder budget measured at ≈ 1080p60 in total (two 1080p30 at full rate) | +| Set-top box | SberBox SBDV-00001 | custom | 4.9.228 | 1.0x | | +| Set-top box | X96Q (Allwinner H313) | 10 | 4.9.170 | 1.0x | | +| Projector | SIBIRLINK HY300 / HY320 | 11 | 4.19.323 | v2 2.02 | 1080p H.264 / H.265 work; 2304×1296 and 2560×1440 do not start ([#165](https://github.com/OpenIPC/decoder/issues/165), [#166](https://github.com/OpenIPC/decoder/issues/166)) | +| TV | Haier Candy Android TV 2K | 11 | 4.9.243+ | 1.0x | No icon in the device's app list | +| TV | Yandex TV Novex NVX-55U169TSY | 7.1.1 | 4.4.3 | 1.0x | | +| Car | Media Center JCAC10003 (ac8227/ac8229) | 12 | 3.18.79+ | 1.0x | | +| Car | Mirror Z55 | 8.1.0 | 4.4.83 | 1.0x | | + +## Not working + +| Type | Device | Android | Kernel | App | Notes | +|-------------|----------------|---------|---------|------|----------| +| Set-top box | Unnamed TV box | 5.1.1 | 3.14.29 | 1.0x | No video | + +## Hardware decoder notes + +What the app has measured or met on real devices. The decoders of a device are listed in `/stats.json` (`decoders`: `name`, `type`, `hardware`, `adaptive`, `max_instances`, `fps_1080p`; see [debug.md](debug.md#decoders)). + +| Topic | Note | +|---|---| +| Decoder budget | The AG-216 (Allwinner H313/H616 family, Android 10) decodes about 1080p60 in total: two 1080p30 streams at full rate. Multi-view learns the budget of each device by measurement, see [architecture.md](architecture.md#decode-planner) | +| Instance limit | The AG-216 allows 4 hardware H.265 decoder instances. Key-frame cells beyond that use a software decoder (`· software`); beyond that too, `Decoder limit: N × H.265` | +| Stuck decoder | The AG-216's video decoder can stop responding until the device is restarted: frames arrive, none is decoded. The app says so on screen and in `/stats.json` (`decoder_stuck`). Cause unknown ([#145](https://github.com/OpenIPC/decoder/issues/145)) | +| MJPEG | Hardware JPEG decoding is used where the device offers `video/jpeg` (also `video/mjpeg`, `video/x-motion-jpeg`). The AG-216 declares an Allwinner MJPEG decoder that Android does not list and that cannot be created; MJPEG is then decoded with BitmapFactory ([#132](https://github.com/OpenIPC/decoder/issues/132)) | +| Window buffer stalls | On Mali TV boxes the app's window buffer stalled for seconds with every grid/single switch while the single view used a `TextureView`; since 2.09 the single view is a `SurfaceView` ([#76](https://github.com/OpenIPC/decoder/issues/76)) | +| Large resolutions | The SIBIRLINK HY300 / HY320 projector plays 1080p H.264 / H.265, but 2304×1296 and 2560×1440 do not start ([#165](https://github.com/OpenIPC/decoder/issues/165), [#166](https://github.com/OpenIPC/decoder/issues/166)) | +| Old Android | Android 5.0 is the minimum. Encrypted URL storage needs 6.0, multicast 8.0, screenshots and snapshots 7.0 | diff --git a/docs/security.md b/docs/security.md new file mode 100644 index 00000000..299ab684 --- /dev/null +++ b/docs/security.md @@ -0,0 +1,73 @@ +# Security + +- [Summary](#summary) +- [Network](#network) +- [Credential store](#credential-store) +- [Debug server](#debug-server) +- [Log redaction](#log-redaction) +- [WebUI](#webui) +- [App permissions](#app-permissions) +- [Reporting](#reporting) + +## Summary + +The app is a viewer for cameras on a network you trust. Nothing it sends or receives is encrypted. Passwords are kept encrypted on the device and are not shown on the debug page or in the log. + +## Network + +- RTSP, RTP over UDP and multicast, HTTP MJPEG and the debug page are plain text. RTSPS, HTTPS and SRTP are not supported, and RTSPS is not planned ([#16](https://github.com/OpenIPC/decoder/issues/16)). +- Credentials go in the `Authorization` header: Basic (the password readable by anyone on the path) unless the camera asks for Digest. They are not put in the RTSP request line. +- Credentials that a camera expects in the URL path or query (for example `user=…_password=…`) are sent with every request. +- A `udp://` slot accepts RTP from any sender unless the URL names one (`udp://@…`); the sender filter compares addresses, which can be spoofed on the same network. +- The app allows clear-text traffic (`usesCleartextTraffic="true"`), which cameras need. + +Use the app on trusted networks only, or inside a secure channel of your own (a VPN to the camera network). + +## Credential store + +- Camera URLs, passwords included, are stored in the app's private `SharedPreferences`, sealed with **AES-256-GCM** (12-byte IV, 16-byte tag) under a key that is generated in and never leaves the **Android Keystore** (alias `camera_urls`, Android 6.0+). Sealed values start with `enc1:`. +- On Android 5.x, or when the Keystore is unavailable or fails, URLs are stored in plain text, as before encryption existed. Plain URLs from older versions are re-saved encrypted on the next start. +- A stored URL that cannot be decrypted (for example after the Keystore lost its key) is kept as it is, not overwritten, until the slot is set again. +- Backup is disabled (`allowBackup="false"`): the settings do not leave the device through Android backup. +- **Reset** removes the stored URLs and deletes the Keystore key, so old values can never be read again; uninstalling removes both too. + +## Debug server + +The [debug server](debug.md) on port 8085 has no password and no encryption. It is always on while the app runs, and: + +- answers only local-network clients: loopback, link-local (169.254/16, fe80::/10), the IPv4 private ranges 10/8, 172.16/12, 192.168/16, and IPv6 unique-local fc00::/7. Every other address gets `403 Forbidden: local network only`; +- is read-only (`GET` only) and has no setting that changes the app; +- never shows a login or password: URLs, statuses and log lines are [redacted](#log-redaction), and the snapshot shows the picture and the cell texts only. + +Anyone on the same LAN can see the camera pictures and hosts through it. On a network you do not trust, close the app when you do not need it. + +## Log redaction + +Applied to every line before it is kept for `/log` and to every string in `/stats.json`: + +| Pattern | Becomes | +|---|---| +| `rtsp://user:pass@`, `rtsps://`, `http://`, `https://` userinfo | `rtsp://***@` | +| `pass=`, `password=`, `pwd=` parameters | `=***` | +| `user=`, `username=`, `login=`, `usr=` parameters (debug server and `/log`) | `=***` | +| `Authorization: ` | `Authorization: ***` | + +The app's own log calls redact URLs the same way, without the login rule ([#55](https://github.com/OpenIPC/decoder/issues/55)), and a shared link is logged by its host only: `adb logcat` shows no passwords, but may show a login that the camera expects in the URL path. Check reports before posting anyway. + +## WebUI + +The built-in browser opens the camera's web interface with JavaScript enabled. HTTP authentication requests are answered with the credentials of the slot URL only for the camera's own host; a page reached by a redirect or a link to another host gets none. + +## App permissions + +| Permission | Why | +|---|---| +| `INTERNET` | streams, WebUI, the debug server | +| `CHANGE_WIFI_MULTICAST_STATE` | the multicast lock for multicast RTP on Wi‑Fi | +| `WRITE_EXTERNAL_STORAGE` (Android ≤ 9), `READ_EXTERNAL_STORAGE` (≤ 12) | screenshots to `Pictures/OpenIPC` on older Android versions | + +No location, camera, microphone or account permissions. No analytics and no Google services. + +## Reporting + +Report security problems as a [GitHub issue](https://github.com/OpenIPC/decoder/issues), without real credentials or public addresses, or through the [OpenIPC channels](https://openipc.org/our-channels) if the details should not be public. diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 00000000..ca2e03f5 --- /dev/null +++ b/docs/testing.md @@ -0,0 +1,90 @@ +# Testing and releases + +- [Branches](#branches) +- [CI](#ci) +- [Unit tests](#unit-tests) +- [Versions and releases](#versions-and-releases) +- [Testing a branch build](#testing-a-branch-build) +- [Stack traces and mapping.txt](#stack-traces-and-mappingtxt) +- [Building locally](#building-locally) +- [Device tests](#device-tests) + +## Branches + +| Branch | Status | +|---|---| +| `v2` | **Main development branch** (default). Modular code in `android/`. Built as `decoder-v2.apk` | +| `basic` | Legacy single-file version (1.0x), kept for reference and older setups. Built as `decoder.apk`. Its version history is in the [`basic` README](https://github.com/OpenIPC/decoder/tree/basic#version-history) | + +Base pull requests on `v2`. + +## CI + +`.github/workflows/build.yml` runs on every push and pull request, and by hand (`workflow_dispatch`, optionally for a given commit). A newer push to the same branch cancels the older run, so `latest` always gets the build of the newest commit. + +| Job | Runs on | What it does | +|---|---|---| +| `test` | every push and pull request, forks included (no secrets) | `./gradlew testDebugUnitTest`, then `./gradlew lintDebug` against `app/lint-baseline.xml` (only new findings fail). Reports are uploaded when it fails | +| `build` | pushes and manual runs, not pull requests | signed `assembleRelease` (R8 minify and resource shrinking), the R8 `mapping.txt` kept as a workflow artifact | + +What a `build` run publishes: + +| Branch | Output | +|---|---| +| `v2` | `decoder-v2.apk` and `decoder-v2-mapping.txt` in the [`latest`](https://github.com/OpenIPC/decoder/releases/tag/latest) release; a [versioned release](#versions-and-releases) for a new version; a message with the APK to the OpenIPC Telegram channel | +| `basic` | `decoder.apk` and its mapping in `latest`; Telegram | +| any other | workflow artifacts `decoder-` (APK) and `…-mapping` on the run page | + +A pull request therefore gets tests and lint only; its APK comes from the push run of its branch. + +## Unit tests + +JVM tests under `android/app/src/test/java/com/openipc/decoder/` (JUnit 4, Android stubs return defaults). They cover the parts that are pure logic: RTSP responses, URLs, authentication and sessions against a fake server (`RtspSession…`, `RtspAuth…`, `RtspUrl…`), SDP and transports, RTP reordering and `udp://` URLs, NAL and JPEG assembly, HTTP MJPEG parsing, audio formats and decoding, the decode planner, the credential store, the debug server (JSON, request parsing, redaction), stream checks, the grid layout, video fit and the buffer pool. No test needs a device; UI and MediaCodec behaviour are tested on the bench ([Device tests](#device-tests)). + +CI is the reference: the project does not depend on local builds. + +## Versions and releases + +- `v2` is **2.xx** (from 2.00). Every pull request that changes the app bumps `android/app/build.gradle` as its last commit before the merge: `versionName` +0.01, `versionCode` +1. Docs-only and CI-only pull requests do not. Current: **2.10** (code 35). +- `basic` keeps its 1.07.x line. +- The first `v2` build of a new `versionName` creates the release **`v`** at the commit it built, with `decoder-v2-.apk`, `mapping-.txt` and notes generated from the merged pull requests since the previous `v2.*` release (by version number). It is not marked *Latest*: the README links `releases/tag/latest`. +- A push without a version bump updates only `latest`. A later build of the tagged commit adds a missing APK or mapping to an existing release; a build of another commit does not touch it. +- The installed version and build hash show in the menu (under **Exit**, `v2.10, `) and in `/stats.json` (`version`, `git`). +- Every build is `com.openipc.decoder` with the release signature, so a branch build installs over a release and keeps the settings. + +Release notes are the change history: [releases page](https://github.com/OpenIPC/decoder/releases). + +## Testing a branch build + +```sh +B=feat/my-branch +RUN=$(gh run list -R OpenIPC/decoder -b "$B" -e push -L 1 --json databaseId -q '.[0].databaseId') +gh run download "$RUN" -R OpenIPC/decoder -n "decoder-${B//\//-}" +adb install -r "decoder-${B//\//-}.apk" +``` + +A given version: `gh release download v2.10 -R OpenIPC/decoder` (`decoder-v2-2.10.apk`). Check the hash under **Exit** after installing. + +## Stack traces and mapping.txt + +Release builds are obfuscated by R8. To read a stack trace from a crash screen or a log, use the mapping of the same build: `mapping-.txt` from the versioned release, `decoder-v2-mapping.txt` from `latest` (only for the build that is there now), or the `…-mapping` artifact of a branch run. Then, with the Android SDK command-line tools: + +```sh +retrace mapping-2.10.txt stacktrace.txt +``` + +## Building locally + +Not needed for contributing (CI builds every push), but possible: JDK 17 and the Android SDK (platform 36), with `ANDROID_HOME` or `android/local.properties`. + +```bash +cd android +./gradlew testDebugUnitTest # unit tests +./gradlew assembleDebug # → app/build/outputs/apk/debug/app-debug.apk, no keys needed +``` + +A release build needs a keystore at `android/key.jks` and `KEYSTORE_PASS`, `KEY_ALIAS`, `KEY_PASS` in the environment (`./gradlew assembleRelease`). Debug and release builds are signed with different keys: to switch, uninstall first, which removes the saved camera URLs. + +## Device tests + +The bench (devices, cameras, addresses, adb access, scripts, log lines, pitfalls) is described in [LABORATORY.md](../LABORATORY.md). Test rounds run as GitHub issues that follow the structure of [#53](https://github.com/OpenIPC/decoder/issues/53). Reports from users: [devices.md](devices.md#reporting). diff --git a/docs/toys.md b/docs/toys.md new file mode 100644 index 00000000..e9155cbb --- /dev/null +++ b/docs/toys.md @@ -0,0 +1,81 @@ +# Drones and Wi‑Fi toys + +- [How such toys stream](#how-such-toys-stream) +- [Connecting](#connecting) +- [FLOW-UFO](#flow-ufo) +- [An unknown toy](#an-unknown-toy) +- [What to send in a report](#what-to-send-in-a-report) + +## How such toys stream + +Cheap drones, cars and other toys with a Wi‑Fi camera usually work the same way: + +``` + toy (access point, e.g. 192.168.169.1) ◄── Wi‑Fi, no internet ──► phone (DHCP address from the toy) + │ │ + └── video to the client that asked for it, vendor protocol ◄──────┘ the vendor app (or this app) +``` + +- The toy is its own **access point** with a fixed name (often the chip or product family plus a few hex digits) and a fixed address. The phone joins it and gets an address from the toy. +- There is **no internet** on that network. +- The video is often **not a standard stream**: no RTSP, no plain RTP. Many toys send JPEG frames over UDP in their own framing and start only after the client has sent a request; some also expect regular replies while they stream. A standard player then sees nothing, and this app stays in *Buffering*. +- Flight or drive commands go over the same network. **This app never sends any**: it only asks for and receives video. + +Whether a toy can be played here depends on its protocol. Toys that do offer RTSP or HTTP MJPEG play like any camera ([transports.md](transports.md)); others need a transport of their own, like [FLOW-UFO](#flow-ufo). + +## Connecting + +1. Switch the toy on and join its Wi‑Fi on the phone. +2. Android notices that the network has no internet and may ask *This network has no internet access. Stay connected?* Answer **yes** (and *Don't ask again*). Otherwise the phone may drop back to another Wi‑Fi or route traffic over mobile data. +3. Mobile data may stay on, but some phones then send traffic for the toy's address over mobile data; if nothing arrives, try with mobile data off. +4. Close the vendor app: the toy usually streams to one client only. +5. While the phone is only on the toy's network, open the [debug pages](debug.md) on the phone itself: `http://127.0.0.1:8085/stats.json` and `http://127.0.0.1:8085/log`. + +## FLOW-UFO + +| | | +|---|---| +| Wi‑Fi name | `FLOW-UFO-xxxxxx` | +| Camera | BL60x Wi‑Fi camera module | +| Toy address | `192.168.169.1`, UDP port 8800 | +| Slot URL | `flowufo://192.168.169.1:8800` (or `flowufo://` alone) | +| Status | **in testing**, not in the releases: issue [#187](https://github.com/OpenIPC/decoder/issues/187), PR [#188](https://github.com/OpenIPC/decoder/pull/188) | +| Test build | [flowufo-187-test1](https://github.com/OpenIPC/decoder/releases/tag/flowufo-187-test1) (2.10 with the FLOW-UFO transport; installs over the release and keeps the settings) | + +What the toy does, as far as is known: + +- It sends nothing until a client sends it a short **probe** on UDP port 8800. The app repeats the probe until video arrives, and starts probing again when the toy goes quiet for 3 s. +- The video is **MJPEG**: each frame is cut into fragments of about 1 KB. The fragments carry the compressed picture without JPEG headers; the app adds standard headers and shows the frames through its normal MJPEG path. +- The client **acknowledges** regularly which fragments it has, and can ask for a frame again. The app does this; whether the toy really needs it is one of the open questions of the test. +- Video only, no sound. No flight command is ever sent. + +The implementation was written from a description of the protocol, not tested on a real toy yet. Status texts while it connects: *Waiting for the toy: probing 192.168.169.1:8800*, *Receiving from the toy at …*, *The toy went quiet: probing … again*. The test build adds a `flowufo` object to `/stats.json` (state, network, frame and fragment counters, losses, resets, errors). + +To test: install the test build, join the `FLOW-UFO-…` Wi‑Fi, [stay connected](#connecting), put `flowufo://192.168.169.1:8800` into a slot (**Settings**, or share the text to the app). If there is no picture, or it looks wrong, wait about 30 s and [send a report](#what-to-send-in-a-report) in [#187](https://github.com/OpenIPC/decoder/issues/187). + +## An unknown toy + +First find the toy's address: on Android it is usually the gateway of the Wi‑Fi (*Wi‑Fi → the network → Details*; often `192.168.0.1`, `192.168.1.1`, `192.168.4.1`, `192.168.169.1`, `172.16.10.1` or `192.168.25.1`). Then try, in a slot: + +| Try | Why | +|---|---| +| `rtsp://:554/` | some toys run a plain RTSP server; try the paths `/live`, `/stream`, `/11`, `/h264` too | +| `rtsp://:8554/` | another common RTSP port | +| `http://:8080/?action=stream` | MJPEG streamers (mjpg-streamer style) | +| `http:///` and `http://:81/stream` | MJPEG on the web port; open them in a browser first to see what answers | +| `udp://@:` | only if the toy pushes RTP by itself to a port of the phone, which is rare | + +If nothing plays, the toy most likely uses a protocol of its own, which needs a [report](#what-to-send-in-a-report) and a transport written for it. + +## What to send in a report + +Open an [issue](https://github.com/OpenIPC/decoder/issues) (one per toy model) with: + +1. The **Wi‑Fi name** of the toy (the part before the varying digits is enough) and the toy's make and model, with a photo of the box or label if possible. +2. The **vendor app**: its name, version and where it comes from (Google Play link). +3. The phone model and Android version, the app version and hash (menu, under **Exit**). +4. What you tried and what the app showed. +5. `http://127.0.0.1:8085/stats.json` and `http://127.0.0.1:8085/log`, taken on the phone after about 30 s of trying. +6. A **traffic capture** while the vendor app shows video: for example with [PCAPdroid](https://github.com/emanuele-f/PCAPdroid) on the phone (no root needed), filtered to the vendor app, saved as a `.pcap` file. A minute is enough. This shows what the toy expects and sends. + +Do not send the vendor app itself or parts of it. The capture contains only the traffic between the phone and the toy; it has no passwords of yours if only the vendor app is captured. diff --git a/docs/transports.md b/docs/transports.md new file mode 100644 index 00000000..5bc7c690 --- /dev/null +++ b/docs/transports.md @@ -0,0 +1,135 @@ +# Transports + +- [Overview](#overview) +- [RTSP](#rtsp) +- [RTSP transport modes](#rtsp-transport-modes) +- [UDP push (Majestic outgoing)](#udp-push-majestic-outgoing) +- [HTTP MJPEG](#http-mjpeg) +- [Toys: FLOW-UFO](#toys-flow-ufo) +- [Codecs](#codecs) +- [Authentication](#authentication) +- [What the servers support](#what-the-servers-support) + +## Overview + +A slot holds one URL. Its scheme picks the path the video takes; for `rtsp://` the slot's transport mode (chosen under the URL in **Settings**, or with the `transport` extra of a [shared link](usage.md#opening-a-camera-from-another-app)) picks how RTP travels. + +| URL | Path | Single view | Multi-view | Audio | +|---|---|---|---|---| +| `rtsp://` | RTSP session, RTP over TCP, UDP or multicast | yes | yes | yes | +| `udp://`, `rtp://` | plain RTP pushed by the camera, no RTSP | yes | yes | no | +| `http://` | MJPEG, `multipart/x-mixed-replace` | yes | yes | no | +| `flowufo://` | toy drone protocol, **test build only** ([#187](https://github.com/OpenIPC/decoder/issues/187)) | yes | yes | no | + +The transport in use is shown when a stream starts in the single view (`Connected • UDP • H.265 …`), on the [debug page](debug.md) and in `/stats.json` (`transport`; the slot's setting is `transport_mode`). After a fallback it reads, for example, `TCP (auto)`. + +

+ Single view right after connecting: transport, codecs and bitrate +

+ +## RTSP + +``` +rtsp://[user:password@]host[:port]/path +``` + +- Port 554 when none is given. +- `DESCRIBE`, `SETUP` (video, and audio when there is a supported audio track), `PLAY`; keepalive at half the session timeout the server announces. +- The video track is the first one offering H.264 or H.265 (also the legacy payload types 96/97 without `rtpmap`), otherwise MJPEG (RTP/JPEG, RFC 2435, static payload type 26). A session description with neither fails the connection, so the back-off applies instead of a black view. +- A server that omits `Content-Length` on the SDP is read until the line goes quiet (300 ms, at most 64 KiB). +- Connect and handshake time out after 5 s each. Without usable video packets for 8 s the watchdog reconnects. Reconnects back off exponentially from 1 s to 30 s. +- Packets over UDP are put back in order by a reorder buffer (the lab cameras reorder by up to ~42 positions). A NAL unit with a lost packet is dropped, see `nal_dropped` in [debug.md](debug.md#statsjson-fields). + +## RTSP transport modes + +| Mode | How the video arrives | Fallback | +|---|---|---| +| **TCP** | RTP interleaved in the RTSP connection | none needed; works through NAT and firewalls | +| **UDP** | RTP over UDP, a port pair per track | never falls back. Lost packets are skipped, not resent | +| **Auto** (default for a new slot) | UDP, else TCP | to TCP when the camera refuses UDP in `SETUP`, when no video arrives within 3 s, or when packets arrive but key frames keep losing a piece | +| **Multicast** | RTSP multicast (Android 8.0+) | to TCP in the same connection when the server refuses it or answers with unicast | + +- After a fallback the URL is remembered as failed for that mode for 10 minutes while the app runs: reconnects, a resume or a rebuilt grid go straight to TCP; after that UDP (or multicast) is tried again. +- If the server answers a UDP request with interleaved TCP, the app takes what the server chose. +- An unknown stored or shared mode value means TCP. +- On Android 5–7 a Multicast slot plays over TCP. + +## UDP push (Majestic outgoing) + +A slot can receive RTP that the camera pushes by itself, without RTSP. Put a `udp://` (or `rtp://`) URL into the slot: + +``` +udp://[source@][address]:port[?codec=h264|h265][&pt=N] +``` + +| URL | Meaning | +|---|---| +| `udp://@:5600` | Listen on port 5600 | +| `udp://@239.1.2.3:5600` | Join the multicast group 239.1.2.3 on port 5600 | +| `udp://192.168.1.10@239.1.2.3:5600` | The same, but accept packets from this sender only | +| `udp://@:5600?codec=h265&pt=97` | Force the codec or the video payload type | + +| Option | Values | Default | +|---|---|---| +| `codec` | `h264` / `avc`, `h265` / `hevc` | from the payload type: 96 → H.264, 97 → H.265 | +| `pt` | 0–127 | 96 or 97 | + +- Addresses must be numeric (IPv4 or IPv6 in brackets); no name lookup. A port is required. +- A codec change on the camera is followed while playing. Video only. +- Two slots cannot listen on the same port. The [U cell](usage.md#the-u-cell) listens on 5600. +- On Wi‑Fi the app holds a multicast lock while it is in a group. +- A pushed stream cannot fall back to TCP: when every key frame loses a piece, the cell decodes the damaged frames (`· damaged`). + +On the camera, add the device as an `outgoing` destination in `/etc/majestic.yaml`, then restart Majestic: + +```yaml +outgoing: + servers: + - udp://192.168.1.20:5600 +``` + +`/api/v1/outgoing.json` on the camera shows whether the destination is live. Majestic can also send to a multicast group (`udp://239.1.2.3:5600`), so several devices can watch one camera. + +## HTTP MJPEG + +``` +http://[user:password@]host[:port]/path +``` + +For example Majestic's `http://user:password@/mjpeg`: a `multipart/x-mixed-replace` stream with one JPEG per part, chunked or not. Port 80 when none is given. Basic credentials are sent with the request, Digest after the camera asks for it. The stream reconnects like RTSP. It plays in the single view and in multi-view; the transport shows as `HTTP`. No audio on this path. `https://` is not supported. + +## Toys: FLOW-UFO + +`flowufo://192.168.169.1:8800` (or `flowufo://` alone) plays the video of toy drones with a `FLOW-UFO-xxxxxx` Wi‑Fi access point. It exists only in a test build so far ([#187](https://github.com/OpenIPC/decoder/issues/187), PR [#188](https://github.com/OpenIPC/decoder/pull/188)), not in the releases. Setup and status: [toys.md](toys.md#flow-ufo). + +## Codecs + +| Kind | Codecs | Decoding | +|---|---|---| +| Video | H.264, H.265 | MediaCodec (hardware); a software decoder for key-frame cells beyond the hardware instance limit | +| Video | MJPEG (RTP/JPEG over RTSP, HTTP MJPEG) | the device's JPEG decoder (`video/jpeg`) where it can be created, otherwise BitmapFactory | +| Audio | AAC (RFC 3640 `mpeg4-generic`, ADTS, MP4A-LATM with out-of-band config), Opus | MediaCodec | +| Audio | G.711 μ-law / A-law, L16 PCM (mono or stereo) | converted in the app | + +Not supported: G.726, MP4A-LATM with in-band configuration (`cpresent=1`) ([#14](https://github.com/OpenIPC/decoder/issues/14)). An unsupported audio track is not set up; the video plays without sound. + +## Authentication + +- Credentials come from the URL (`user:password@`) and go in the `Authorization` header: Basic first, Digest when the camera asks for it. They are not put in the RTSP request line. +- Credentials that a camera expects in the URL path or query (for example XM's `user=…_password=…`) are sent as part of the URL with every request. +- Nothing is encrypted: see [security.md](security.md#network). + +## What the servers support + +Measured on the lab cameras; details and dates in [LABORATORY.md](../LABORATORY.md#what-the-servers-support). + +| Feature | OpenIPC Majestic | XM (`H264DVR 1.0`) | +|---|---|---| +| RTP over TCP (interleaved) | yes | yes | +| RTP over UDP | yes | yes (reply without `/UDP`) | +| RTSP multicast | no: `400` to a multicast request, silent unicast when the request also has `client_port` | no: `451`, silent unicast likewise | +| Plain RTP push (`outgoing`), unicast or multicast | yes | not known | +| HTTP MJPEG | yes (`/mjpeg`) | not tested | +| Audio | Opus, AAC, L16 (G.711 not testable on the lab build) | none (video H.264 only) | + +With a Multicast slot both play over TCP in the same connection. Multicast from OpenIPC is available only as plain RTP via `outgoing`. A server that does RTSP multicast (for example mediamtx) is needed to use the Multicast mode ([#69](https://github.com/OpenIPC/decoder/issues/69)). diff --git a/docs/usage.md b/docs/usage.md new file mode 100644 index 00000000..cf6bbde0 --- /dev/null +++ b/docs/usage.md @@ -0,0 +1,121 @@ +# Usage + +- [First camera](#first-camera) +- [Menu](#menu) +- [Gestures and keys](#gestures-and-keys) +- [Remote control](#remote-control) +- [Multi-view](#multi-view) +- [Slides](#slides) +- [The U cell](#the-u-cell) +- [WebUI](#webui) +- [Opening a camera from another app](#opening-a-camera-from-another-app) +- [Screenshots](#screenshots) +- [Reset](#reset) + +## First camera + +1. Open the menu: tap the screen, or press **OK** on a remote. +2. Choose a slot (**1**–**8**), then **Settings**. +3. Enter the camera URL, for example `rtsp://user:password@192.168.1.10:554/stream=0`, and choose the transport under it (**TCP / UDP / Auto / Multicast**, a new slot starts on Auto). URL formats: [transports.md](transports.md). +4. **Save**. The app connects at once and reconnects by itself when the stream drops (exponential back-off, 1 s up to 30 s). + +The URL dialog is titled `Camera N URL`. The picture is fitted to the screen whatever its shape, without cropping. URLs are stored encrypted, see [security.md](security.md#credential-store). + +## Menu + +The menu is one grid: the eight slots on top, two rows of four below. Every word cell is two slots wide. + +``` +[ 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 ] +[ Settings | WebUI | Grid | Slides ] +[ Exit | Reset | Debug | UDP ] +``` + +

+ The menu on a phone +

+ +| Item | Action | +|---|---| +| **1**–**8** | Switch to a camera slot (single view). The number of the active slot shows the connection quality: green under 100 ms of jitter, yellow 100–300 ms, red above. A grey number is an empty slot | +| **Settings** | URL and transport of the active slot. Single view only, grey elsewhere | +| **WebUI** | The camera's web interface, see [WebUI](#webui). Single view only, grey elsewhere | +| **Grid** | The view: [multi-view](#multi-view) (2×2 or 3×3) or the single camera. Does not change the playback mode | +| **Slides** | The playback mode: every frame, or key frames only, see [Slides](#slides). Does not change the view | +| **Exit** | Close the app. The small text under it is the version and build hash (`v2.10, `) | +| **Reset** | Clear all settings after a confirmation, see [Reset](#reset) | +| **Debug** | Text in the corner of every multi-view cell (resolution, codec, decoded/received fps, key frames, status), or none | +| **UDP** | [The U cell](#the-u-cell) full-screen | + +Items that are on (Grid, Slides, Debug, UDP and the active slot) have an orange fill; the item the remote is on has a light blue frame. On a narrow screen or with a large font the labels are shortened with an ellipsis rather than clipped. + +## Gestures and keys + +| Input | Action | +|---|---| +| Tap / **OK** / **Enter** (D-pad centre) | Open or close the menu | +| **Back** / **Esc** | Close the menu or a dialog; without one, Back keeps its system action | +| Pinch, then drag | Zoom (1× to 5×) and pan, single view | +| Double tap | Reset the zoom | +| Long press, single view | Save a screenshot, see [Screenshots](#screenshots) | +| Long press, multi-view | Play this cell's sound; again to mute | + +## Remote control + +The app is in the Leanback launcher of Android TV devices and does not require a touchscreen. **OK** opens the menu; the D-pad moves through the grid; the first key after a menu opened by touch moves the focus to the active item. In the reset dialog **Cancel** has the focus, so a stray **OK** deletes nothing. + +## Multi-view + +**Grid** shows the configured slots in a 2×2 grid, or in a 3×3 grid once any of slots 5–8 is used. The ninth cell of the 3×3 grid is [the U cell](#the-u-cell). Tap any cell to open the menu. + +

+ Multi-view with eight cameras and the menu +

+ +Each cell has its own connection and decoder. The device's video decoder has a fixed budget (the AG-216 test box decodes about two 1080p30 streams at full rate), so the grid adapts: + +- Cells start on key frames only, then decode every frame in priority order while the decoder keeps up: the U cell while it receives a stream, the cell whose sound plays, then slot order. +- Cells beyond that stay on key frames: the picture updates once per key frame interval (`key frames` in the corner text). MJPEG cells show one frame per second instead (`reduced`). +- The limit is learnt per device by measurement and kept across restarts. How: [architecture.md](architecture.md#decode-planner). +- A key-frame cell beyond the hardware decoder's instance limit (the AG-216 has 4 × H.265) uses a software decoder (`· software` in the corner). Only beyond that does it show `Decoder limit: N × H.265`. +- When even key frames are too much, the lowest-priority cell is paused (`Paused: decoder overloaded`) and tried again after a quiet period. Losses in the first 10 s after the grid starts do not count. +- A cell whose UDP stream loses a piece of every key frame shows the frames with the damage concealed (`· damaged`) rather than a black picture. +- When frames arrive in several cells for 20 s and none is decoded, the app says that the device's video decoder does not respond (seen on the AG-216, [#145](https://github.com/OpenIPC/decoder/issues/145)); only a restart of the device helps. + +Substreams of the cameras, where available, let more cells play at full rate. Some cameras accept only one RTSP client at a time. + +**Audio:** long-press a cell to play its sound, long-press it again to mute. The selected cell shows an **AUDIO** badge and is remembered. + +## Slides + +**Slides** decodes key frames only, in multi-view and in the single view: the lowest decoder load, with the picture updated once per key frame interval (MJPEG once a second). It never changes the view, and **Grid** never changes Slides; both may be on. `/stats.json` reports the mode as `"mode": "slides"` in multi-view and `"decode": "key frames"`. + +## The U cell + +The U cell plays a camera that pushes RTP over UDP to port **5600** of the device (`udp://@:5600`), for example OpenIPC Majestic with an `outgoing` destination, see [transports.md](transports.md#udp-push-majestic-outgoing). Until a stream arrives it shows every address of the device to send to. **UDP** in the menu opens it full-screen. Other ports or multicast groups go into a slot as a `udp://` URL. + +## WebUI + +**WebUI** opens the camera's web interface in a built-in browser, with the credentials taken from the slot's RTSP URL. It uses the system WebView: it needs a working WebView package (on some devices Google's *Android System WebView*); without it only WebUI is affected ([#15](https://github.com/OpenIPC/decoder/issues/15)). On a touch device the page scrolls the focused field above the on-screen keyboard. **Close** closes it. + +

+ WebUI on a phone: the page scrolls the field above the keyboard +

+ +## Opening a camera from another app + +The app accepts plain-text shares (`ACTION_SEND`, `text/plain`). Sharing an `rtsp://`, `udp://` or `http://` URL from another app, for example OpenIPC Network, puts it into the **active** slot, saves it and switches to the single view. An optional `transport` extra (`tcp`, `udp`, `auto`, `multicast`) sets the slot's transport; without it the slot keeps its setting. + +```sh +adb shell am start -a android.intent.action.SEND -t text/plain \ + --es android.intent.extra.TEXT 'rtsp://user:pass@192.168.1.10/stream=0' \ + --es transport udp -n com.openipc.decoder/.Decoder +``` + +## Screenshots + +A long press in the single view saves the picture to `Pictures/OpenIPC` (at most 3840×2160; a larger stream is scaled down). Screenshots need Android 7.0 or newer; Android 7–9 ask for the storage permission first. For a picture of the whole grid from another computer use `/snapshot.jpg` of the [debug page](debug.md#snapshot). + +## Reset + +**Reset** asks *Start with a clean slate?*; **Clear settings** removes all camera slots, their transports and the other settings, and the app restarts as right after installing. The cameras are not changed. **Cancel** has the focus.