diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index b2355f0f..69f52052 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -59,3 +59,58 @@ jobs: dist/openstream-android.apk.sha256 dist/android-signing.txt if-no-files-found: error + + signed-candidate: + if: github.event_name == 'workflow_dispatch' + name: Release-signed update APK + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: ./.github/actions/android-setup + - name: Decode release signing key + env: + KEYSTORE_BASE64: ${{ secrets.OPENSTREAM_RELEASE_KEYSTORE_BASE64 }} + run: | + test -n "$KEYSTORE_BASE64" + printf '%s' "$KEYSTORE_BASE64" | base64 --decode > "$RUNNER_TEMP/openstream-release.keystore" + - name: Build release-signed update APK + working-directory: android + env: + OPENSTREAM_RELEASE_KEYSTORE: ${{ runner.temp }}/openstream-release.keystore + OPENSTREAM_RELEASE_STORE_PASSWORD: ${{ secrets.OPENSTREAM_RELEASE_STORE_PASSWORD }} + OPENSTREAM_RELEASE_KEY_ALIAS: ${{ secrets.OPENSTREAM_RELEASE_KEY_ALIAS }} + OPENSTREAM_RELEASE_KEY_PASSWORD: ${{ secrets.OPENSTREAM_RELEASE_KEY_PASSWORD }} + run: ./gradlew --no-daemon :app:assembleRelease + - name: Check APK metadata and signing continuity + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + apk=android/app/build/outputs/apk/release/app-release.apk + aapt="$ANDROID_SDK_ROOT/build-tools/35.0.0/aapt" + apksigner="$ANDROID_SDK_ROOT/build-tools/35.0.0/apksigner" + "$apksigner" verify "$apk" + metadata=$("$aapt" dump badging "$apk") + grep -q "name='dev.openstream.app'" <<< "$metadata" + mkdir -p "$RUNNER_TEMP/previous-release" dist + gh release download v1.0.1 --pattern openstream-android.apk --dir "$RUNNER_TEMP/previous-release" + current_cert=$("$apksigner" verify --print-certs "$apk" | sed -n 's/.*certificate SHA-256 digest: //p') + previous_cert=$("$apksigner" verify --print-certs "$RUNNER_TEMP/previous-release/openstream-android.apk" | sed -n 's/.*certificate SHA-256 digest: //p') + test "$current_cert" = "$previous_cert" + previous_metadata=$("$aapt" dump badging "$RUNNER_TEMP/previous-release/openstream-android.apk") + current_code=$(sed -n "s/^package:.*versionCode='\([^']*\)'.*/\1/p" <<< "$metadata") + previous_code=$(sed -n "s/^package:.*versionCode='\([^']*\)'.*/\1/p" <<< "$previous_metadata") + test "$current_code" -gt "$previous_code" + python tools/check_android_16kb_alignment.py "$apk" + cp "$apk" dist/openstream-android-update.apk + (cd dist && sha256sum openstream-android-update.apk > openstream-android-update.apk.sha256) + - uses: actions/upload-artifact@v4 + with: + name: openstream-android-signed-candidate + path: | + dist/openstream-android-update.apk + dist/openstream-android-update.apk.sha256 + if-no-files-found: error diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a4b50371..d9476685 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -366,4 +366,4 @@ jobs: dist/openstream-obs-linux-x86_64.tar.gz.sha256 \ --target "$RELEASE_COMMIT" \ --title "OpenStream ${TAG_NAME}" \ - --notes-file docs/release-notes-template.md + --generate-notes diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index dbf2597c..00000000 --- a/AGENTS.md +++ /dev/null @@ -1,56 +0,0 @@ -# OpenStream Repository Rules - -## Product objective - -OpenStream is one product made from the Android camera app and the native Linux -OBS plugin. Keep changes focused on making that complete Android-to-OBS path -work reliably: discovery, pairing, reservation, camera controls, SRT media, -disconnect, and reconnect must agree across both sides. A change to one side -must preserve or deliberately update its counterpart and the protocol docs. - -The Android app must be straightforward to update during development and in -releases. Keep its `applicationId` stable, keep update signing consistent, and -use one documented version source rather than manually changing version -strings in unrelated files. Before changing the application ID or signing -identity, establish why existing installations cannot be upgraded and document -the migration impact. Version code must increase for releases; development APKs -should have a clear install/update command and explain signature or downgrade -errors when those prevent in-place installation. - -Improve the UI and UX around the actual camera workflow: show clear connection, -reservation, streaming, and failure states; make connect/disconnect and phone -selection discoverable; and keep Android and OBS labels/actions consistent. -Prefer fixes tied to observed user friction over broad visual changes that do -not improve the end-to-end workflow. - -For each change, check which part of the Android-to-Linux OBS workflow it -affects, preserve compatibility where required, and leave the system in a -state that can be built, installed, and manually exercised. Do not claim the -whole product works based only on unit or contract tests; report which parts -were actually verified and which require a physical Android device or OBS. - -These rules apply to every OpenStream change. - -1. Never replace source timestamps with arrival time. -2. Never re-encode ISO video. -3. Never use unbounded queues. -4. Never put OBS dependencies inside the engine. -5. Never let the UI own media sessions. -6. Never perform network or disk work on the UI thread. -7. Never perform ordinary CPU frame copies in the hardware path. -8. Never allow one camera failure to interrupt another. -9. Never enable a software codec silently. -10. Never create a second phone video encoder in standard mode. -11. Never mix all microphones automatically. -12. Never promise genlock or guaranteed frame-perfect sync. -13. Keep unrelated changes in separate PRs. -14. Every performance change must include before-and-after measurements. -15. Every media change must include behavioural tests. -16. Every queue must declare its capacity and overflow policy. -17. Every recording gap must be surfaced and logged. -18. Hardware acceleration must have an explicit fallback and warning. -19. Preserve legacy compatibility through adapters, not core contamination. - -## Completion workflow - -After completing a task, create a pull request on GitHub for the finished changes. diff --git a/AGENT_PROGRESS.md b/AGENT_PROGRESS.md deleted file mode 100644 index 143122c8..00000000 --- a/AGENT_PROGRESS.md +++ /dev/null @@ -1,60 +0,0 @@ -# Agent progress - -## Completed since last review - -- Autonomous maintenance is paused at the user's request; this branch is prepared for immediate integration into `main` after the latest validation pass. -- Bound Android camera mutation controls (`/zoom`, `/torch`, `/lens`, `/identify`) and `/release` to the peer that owns the active reservation; cross-peer takeover and unexpected unbound reservation state fail closed. -- Removed permissive browser CORS/preflight access from the native control port and require `Content-Type: application/json` for mutating routes. -- Added a production-like FFmpeg/libSRT caller fault-injection probe and wired OBS to a 4.5 s SRT I/O timeout plus 2 s connect timeout. -- Hardened the SRT timeout probe so FFmpeg diagnostic output cannot fill an unread stderr pipe and distort timeout/recovery measurements. -- Added same-phone reconnect stickiness for auto-selected OBS slots and made the 45 s hold single-shot so failed reserve/open retries cannot extend it. -- Made post-expiry failover deterministic: another eligible phone is preferred after hold expiry, while the failed phone remains sole-candidate fallback. -- Made reconnect recovery explicitly per-attempt: `decode_packets()` now returns the number of video frames successfully output during that invocation, and only that count can satisfy the 30-frame recovery gate. The shared `frames_output` diagnostic counter is no longer reset/reused as reconnect state. -- Bounded OBS camera-control TCP connection establishment to 1 s with nonblocking connect completion checks, preventing an unreachable/blackholed phone from leaving reservation, release, or control work stuck in the OS TCP connect timeout. -- Made duplicate same-source/same-peer `/reserve` retries idempotent on Android so OBS reconnect polling no longer restarts the phone's 45 s reservation-release timer; reservation token rotation still follows the newest retry and real slot/bitrate changes still reach the reservation owner. -- Fixed the OBS selected-but-unreserved teardown race: the auto-phone worker now tracks reservation success explicitly, never marks/opens a selected phone after failed `/reserve`, and queues release if stop lands immediately after a successful reservation. -- Tightened unreachable-phone teardown: executor shutdown now keeps only the newest pending reservation release, rejects new urgent work once stopping begins, and wakes out of retry backoff immediately so OBS destruction does not drain stale release retries. - -## In progress - -- Physical two-phone → Wi-Fi → OBS acceptance testing remains outstanding for reconnect behavior, short/flapping decoded bursts, sustained latency, A/V sync, thermals, controls and Virtual Camera startup. -- The automated suite still does not exercise the complete production plugin path from a hard SRT blackhole through `av_read_frame()` exit, the 500 ms reconnect loop, discovery/reservation reacquisition, and restored media. -- A physical unreachable-phone teardown/release check remains outstanding. The source-destruction path is now structurally bounded to at most the currently executing/newest release attempt instead of draining the full retry backlog. - -## Tests performed - -- Pre-progress head `25763a1`: Android APK and Windows OBS Plugin workflows both passed; this head adds deterministic control-teardown retry tests and contains no runtime/source change relative to `f62ac6c`. -- Latest runtime head `f62ac6c`: Android APK and Windows OBS Plugin workflows both passed. -- Focused CodeRabbit review of `f62ac6c` found no blocking correctness/lifecycle issue in shutdown/retry concurrency or newest-release preservation. -- Added deterministic teardown retry coverage that separately proves normal operation retains the full three-attempt policy and that teardown interrupts the 150 ms retry backoff and stops after the in-flight attempt. -- Added reconnect contracts requiring an explicit per-`decode_packets()` video-frame count, forbidding the recovery gate from reading `ctx->frames_output`, and covering the case where an older/lifetime frame count is already above 30 but the current reconnect produces fewer than 30 frames. -- Added a Windows-CI reservation teardown contract that requires explicit reservation success before `reserved_phone`/SRT activation and requires immediate release when stop follows successful reservation. -- Corrected caller-role FFmpeg/libSRT loopback probe completed successfully for permanent and temporary bidirectional blackholes. -- Sandbox synthetic reconnect-chain probe: hard bidirectional SRT blackhole caused receiver exit in 4,764.3 ms; after the configured 500 ms retry delay, a second reservation attempt plus listener/receiver restart restored decoded frames. This validates the transport/retry sequence in loopback, not the compiled OBS worker or a physical phone. -- Added teardown contracts requiring shutdown to collapse pending urgent releases to the newest token and interrupt the 150 ms retry backoff when stopping. - -## Benchmarks - -- Caller-role hard blackhole with `timeout=4,500,000 us`: 4,830.3 ms, 4,837.8 ms, 4,823.4 ms, 4,744.4 ms and 4,754.6 ms to receiver exit (mean 4,798.1 ms). -- Temporary blackholes recovered on the same FFmpeg receiver after restore: 2.0 s → 21.0 ms; 3.0 s → 105.3 ms; 3.5 s → 187.9 ms; 4.0 s → 10.8 ms to next receiver frame. -- Unreachable control connect is explicitly bounded to 1 s. Before the teardown tightening, destruction could drain up to three failed urgent attempts plus two 150 ms gaps (~3.3 s for a pure connect blackhole); shutdown now stops after the in-flight/newest attempt and skips retry backoff. This is a code-path bound, not a physical timing measurement. -- These are loopback/code-path results, not physical Wi-Fi/phone measurements, and the SRT measurements predate the stderr backpressure hardening. - -## Known problems / regressions - -- Physical phone → Wi-Fi → OBS testing is still needed for sustained latency, A/V sync, thermals, reconnect behavior, Virtual Camera startup, vendor-specific Android camera/codec behavior, and unreachable-phone control teardown. -- The loopback probe validates timeout policy and same-receiver recovery, but not the complete plugin blackhole → `av_read_frame()` exit → reconnect loop → phone reacquisition path. - -## Unresolved review feedback - -- No unresolved inline review threads are currently present. The focused review of the current teardown change produced no blocking comments. -- Macroscope correctness coverage remains limited by its workspace billing limit; this is a review-coverage gap, not a code failure. - -## Inspect before merging - -- Verify an auto-selected slot stays on the same phone during the 45 s hold, repeated failures and short decoded bursts below 30 frames do not reset that reconnect episode, and a healthy alternate is preferred after expiry. -- Verify repeated OBS `/reserve` retries do not keep a disconnected phone busy beyond the original Android 45 s reservation lease, while a deliberate slot/bitrate change still updates the reservation. Automated contracts now prove identical retries return before the `onReserve` callback that increments reservation generation and schedules the Android release timer. -- Verify the failed phone remains usable as fallback when no alternate is eligible and a new hold starts only after sustained media recovery. -- Verify camera controls work from the reserving OBS PC and are rejected from a second LAN device while reserved. -- Verify stopping/removing an OBS source while the phone control IP is unreachable returns promptly; shutdown now collapses stale release work and does not wait through the normal three-attempt retry loop. -- Verify the 4.5 s timeout with a real phone stream blackhole, including the 500 ms reconnect loop and recovery from shorter Wi-Fi interruptions. diff --git a/README.md b/README.md deleted file mode 100644 index 7318a00c..00000000 --- a/README.md +++ /dev/null @@ -1,282 +0,0 @@ -
- - - - OpenStream V1.0.1 Banner - - -### Turn any Android phone into a wireless camera source for OBS Studio - -[![Version](https://img.shields.io/badge/version-1.0.1-00D4AA?style=flat-square&labelColor=1a1a2e)](https://github.com/YashasVM/OpenStream/releases/tag/v1.0.1) -[![Platform](https://img.shields.io/badge/platform-Android%20%2B%20Windows%20%2B%20Linux-green?style=flat-square&labelColor=1a1a2e)](https://github.com/YashasVM/OpenStream) -[![OBS](https://img.shields.io/badge/OBS-Studio%20Plugin-purple?style=flat-square&labelColor=1a1a2e)](https://obsproject.com) -[![Website](https://img.shields.io/badge/website-openstream.pages.dev-00D4AA?style=flat-square&labelColor=1a1a2e)](https://openstream.pages.dev) - -**Open-source** | **One phone, one camera** | **Hardware phone encoding** | **Local Wi-Fi** - ---- - -
- -> [!IMPORTANT] -> This branch simplifies OpenStream to one Android camera per OBS process and adds a native Linux build. Published V1.0.1 downloads predate these changes. Candidate builds need the device acceptance checks in [docs/testing.md](docs/testing.md) before release. - -The supported V1.0.1 path is the legacy Android app plus the legacy OBS camera -source/dock over local Wi-Fi. USB transport and USB camera-source support are -not included in this release. - -## Quick Downloads - -| Step | Download | Install on | -|---|---|---| -| 1 | [`openstream-android.apk`](https://github.com/YashasVM/OpenStream/releases/latest/download/openstream-android.apk) | Android phone | -| 2 | [`openstream-obs-plugin-installer-windows-x64.exe`](https://github.com/YashasVM/OpenStream/releases/latest/download/openstream-obs-plugin-installer-windows-x64.exe) | Windows OBS PC | -| Fallback | [`openstream-obs-windows-x64.zip`](https://github.com/YashasVM/OpenStream/releases/latest/download/openstream-obs-windows-x64.zip) | Manual OBS plugin install | -| Linux candidate | [Build and install from source](docs/linux.md) | Native Linux OBS; the next tagged release will include a matched Linux package | - -Need the non-technical walkthrough with screenshots? Start with [`docs/set-up.md`](docs/set-up.md). - ---- - -## What is OpenStream? - -OpenStream V1.0.1 sends your Android phone camera directly into OBS Studio over local Wi-Fi. It uses Camera2, MediaCodec video/audio encoding, MPEG-TS muxing, SRT transport, two-way LAN discovery, single-camera pairing, and a native OBS source plugin. - -```text -Phone camera -> hardware AVC/H.264 + AAC -> SRT over Wi-Fi -> OpenStream camera source in OBS -``` - -### V1.0.1 Release - -| Area | V1.0.1 release detail | -|---|---| -| **OBS setup** | The V1.0.1 release uses its legacy source controls. The current candidate exposes controls in source properties. | -| **Scene compatibility** | Existing `OpenStream V7` scene sources keep loading while new sources appear as `OpenStream V8`. | -| **Pairing clarity** | One camera per OBS process, with a simple computer picker on Android. | -| **Release polish** | Version defaults, release notes, and setup docs are aligned around V1.0.1. | - -## Quick Start - -### 1. Install the Android App - -Download the APK from the release, copy it to your Android phone, open it, and allow the install prompt. Grant camera and microphone permissions on first launch. - -### Updating a local development build - -Connect the phone with USB debugging enabled and run `cd android && ./gradlew -installDebug`. Repeating this command installs the updated app in place when -the installed build has the same application ID and signing key. The version -name and code come from `android/gradle.properties`; increase the code there -for a release. Android rejects an in-place update if the APK uses a different -signing key, such as when switching between a local debug build and the -published release. Keep using the same build type for day-to-day updates. - -### 2. Install the OBS Plugin - -Download and run `openstream-obs-plugin-installer-windows-x64.exe` on the Windows PC that has OBS Studio installed. Restart OBS after the installer finishes. - -> [!NOTE] -> If you prefer manual installation, download `openstream-obs-windows-x64.zip`, extract it, and run `install-openstream-plugin.bat` as administrator. - -### 3. Add One Camera in OBS - -In OBS, click `+` in Sources, choose `OpenStream V8`, and press OK. Keep one OpenStream source. To use the camera in another scene, select **Add Existing**. If upgrading a scene with multiple camera sources, remove the extras; additional capture sessions are blocked with an explanation. - -### 4. Pair the Phone - -Open the OpenStream Android app on the same Wi-Fi network as the PC. Its discovery panel lists available OBS computers; tap yours to connect. The camera feed should appear in OBS within a few seconds. - -### 5. Stream - -Use OBS as usual. Open **Docks → OpenStream Camera Control** for the camera controls. Phone audio appears on the OpenStream source’s OBS mixer channel. - -> [!TIP] -> Use a 5 GHz or Wi-Fi 6 network, keep both devices on the same subnet, and disable VPNs or router client isolation during first setup. - ---- - -## Compatibility & Limits - -| Area | Current support | -|---|---| -| **Phone OS** | Android 10+ with Camera2 and hardware MediaCodec support. | -| **OBS host** | Windows x64: OBS Studio 32.2.1. Native Linux: build against the installed OBS, Qt, and FFmpeg packages; see [Linux setup](docs/linux.md). Flatpak/Snap builds are not covered. | -| **Network** | Same LAN/subnet; guest Wi-Fi, VPNs, and client isolation can block discovery. | -| **Release maturity** | V1.0.1. Expect device-specific camera quirks and Wi-Fi-dependent latency. | - ---- - -## Features - -### Android App - -| Feature | Details | -|---|---| -| **Full HD Streaming** | Streams a sustainable 1080p30 camera feed over SRT, with a 720p30 fallback profile. | -| **Hardware Encoding** | Uses an explicit hardware AVC/H.264 surface encoder; unsupported hardware is reported instead of silently using software video encoding. | -| **Audio Streaming** | Sends microphone audio with the video stream as AAC. | -| **Multi-Lens Switching** | Supports rear, ultrawide, telephoto, and front cameras when available. | -| **Pinch-to-Zoom** | Smooth digital zoom with a live zoom indicator. | -| **Screen-Off Streaming Mode** | Dims the phone to a black overlay while keeping capture and streaming active. | -| **Torch and Screen Controls** | Keeps the phone awake and can toggle torch while streaming. | -| **OBS Picker** | Choose your OBS computer and see whether the camera is available or connected. | -| **Auto-Discovery** | Finds OpenStream OBS listeners on the same LAN and advertises the phone back to OBS. | -| **Manual Connect** | Enable **Receive manual connection from phone** in OBS advanced settings, then enter the PC IP/port on the phone. Remote camera controls stay on the phone in this mode. | -| **Live Stream Telemetry** | Shows frames, keyframes, transferred megabits, errors, and active lens while streaming. | -| **Identify Overlay** | Displays a connection label on the phone when triggered from OBS. | -| **Reconnect Hold** | Keeps a reserved OBS connection for `45` seconds after a disconnect to make brief Wi-Fi drops less disruptive. | - -### OBS Plugin - -For native Linux build and installation instructions, see [docs/linux.md](docs/linux.md). Linux packages must match the OBS installation; do not copy a native plugin into a Flatpak or Snap installation. - - -| Feature | Details | -|---|---| -| **Native OBS Source** | Adds an `OpenStream V8` source type inside OBS Studio while preserving V7 scene compatibility. | -| **OBS controls** | Connection, phone selection, zoom, torch, lens, and identify controls are exposed through source properties. | -| **One-Click Installer** | Windows installer copies the plugin into the OBS plugin folder. | -| **Phone ownership** | Each phone can be reserved by one OBS source at a time. Reuse that source across scenes. | -| **Phone Discovery** | Lists discovered Android phones, includes a refresh action, and can let the phone choose the OBS computer. | -| **Auto-Connect** | Listens for the Android app and connects without typing IP addresses. | -| **Deep-Link Pairing URL** | Exposes an `openstream://connect` pairing URL with slot, port, latency, and source identity. | -| **Separate Audio Mixer** | Phone microphone audio uses the OpenStream source’s OBS mixer channel. | -| **Remote Controls** | Adjust zoom, torch, rear/front camera, and the phone identify overlay from OBS source properties. | -| **Reconnect Handling** | Reserves and releases phones per source so streams recover into the same OBS source. | - ---- - -## UX Flow - -| Moment | Experience | -|---|---| -| **First launch** | The phone explains camera/microphone access, then presents a guided discovery state over the camera preview. | -| **OBS setup** | Add a camera source and use its properties to select a phone and control the connection. | -| **Pairing** | The phone scans for available OBS computers and provides refresh, empty-state recovery, and manual fallback. | -| **Going live** | A `LIVE` badge, zoom chip, stream stats, and status text make the active connection visible at a glance. | -| **Other scenes** | Choose Add Existing to reuse your camera without opening another session. | -| **On-set checks** | Use `Identify Phone` in OBS to flash the connection label on the physical phone. | -| **Battery and heat** | Use `DISPLAY` for a black screen-off overlay or `STAY` to keep the phone awake. | - ---- - -## Architecture - -```text -Android phone - Camera2 preview/capture - MediaCodec hardware AVC/H.264 video - MediaCodec AAC audio - MPEG-TS muxer - libsrt sender - | - | SRT media stream on port 9100 - v -Windows or native Linux PC - OBS Studio - OpenStream native source plugin - FFmpeg SRT receive/decode - OBS video frame + audio mixer output -``` - -Current candidate defaults (SRT `:9100`, phone control `:9101`, discovery `:51615`, `12 Mbps`, `120 ms`) are canonical in [docs/protocol.md](docs/protocol.md). The standalone Python receiver remains a developer tool and can use a separately selected port. - ---- - -## Building - -### Android App - -Development build: - -```powershell -cd android -$env:JAVA_HOME = 'C:\Program Files\Android\Android Studio\jbr' -.\gradlew.bat :app:assembleDebug -``` - -Debug APK output: - -```text -android/app/build/outputs/apk/debug/app-debug.apk -``` - -> [!NOTE] -> Normal APK builds link bundled libsrt static libraries for real network streaming. Use `-Popenstream.nonStreamingCiBuild=true` only for intentional source-compile checks. - -Signed release build: - -```powershell -$env:OPENSTREAM_RELEASE_KEYSTORE = "$PWD\openstream-release.keystore" -$env:OPENSTREAM_RELEASE_STORE_PASSWORD = "" -$env:OPENSTREAM_RELEASE_KEY_ALIAS = "" -$env:OPENSTREAM_RELEASE_KEY_PASSWORD = "" -.\gradlew.bat :app:assembleRelease -``` - -### OBS Plugin - -For native Linux build and installation instructions, see [docs/linux.md](docs/linux.md). Linux packages must match the OBS installation; do not copy a native plugin into a Flatpak or Snap installation. - - -The easiest local build path on Windows is the included script: - -```powershell -.\build_plugin.bat -``` - -To package without installing: - -```powershell -$env:OPENSTREAM_SKIP_INSTALL = "1" -$env:OPENSTREAM_PLUGIN_PACKAGE_DIR = "$PWD\artifacts" -.\build_plugin.bat -``` - -The package output is `artifacts/openstream-obs-windows-x64.zip`. - ---- - -## Release Notes - -GitHub Actions publishes: - -| Asset | Purpose | -|---|---| -| `openstream-android.apk` | Signed Android app install package | -| `openstream-obs-plugin-installer-windows-x64.exe` | Recommended Windows OBS plugin installer | -| `openstream-obs-windows-x64.zip` | Manual OBS plugin package | - -See [`docs/release.md`](docs/release.md) for release tagging and validation. - ---- - -## Developer Smoke Test - -Validate SRT transport without OBS: - -```powershell -python tools/openstream_receiver.py --port 9100 --latency-ms 120 --ffplay -``` - -This is a developer/debug tool only. Normal users should install the OBS plugin. - ---- - -## Links - -| Link | URL | -|---|---| -| Website | [OpenStream V1.0.1](https://openstream.pages.dev) | -| Releases | [github.com/YashasVM/OpenStream/releases](https://github.com/YashasVM/OpenStream/releases) | -| Issues | [github.com/YashasVM/OpenStream/issues](https://github.com/YashasVM/OpenStream/issues) | - ---- - -
- -**Made by [@YashasVM](https://github.com/YashasVM)** - -*Turn your phone into a pro OBS camera source. No cables required.* - -
diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 359df775..01f40773 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -21,6 +21,13 @@ val openStreamVersionName = providers.gradleProperty("openstream.versionName") val openStreamVersionCode = providers.gradleProperty("openstream.versionCode") .map { it.toInt() } .get() +// Development APKs are commonly shared outside Gradle's install task. Give +// each build a newer code so Android accepts an in-place update with the same +// debug signing key, while releases keep the reviewed code in gradle.properties. +val developmentVersionCode = maxOf( + openStreamVersionCode, + (System.currentTimeMillis() / 1000).toInt(), +) android { namespace = "dev.openstream.app" @@ -66,6 +73,12 @@ android { } buildTypes { + debug { + versionNameSuffix = "-dev" + if (providers.gradleProperty("openstream.testApplicationIdSuffix").orNull == "true") { + applicationIdSuffix = ".test" + } + } release { isDebuggable = false isMinifyEnabled = false @@ -87,6 +100,14 @@ android { } } +androidComponents { + onVariants(selector().withBuildType("debug")) { variant -> + variant.outputs.forEach { output -> + output.versionCode.set(developmentVersionCode) + } + } +} + gradle.taskGraph.whenReady { val releaseTaskRequested = allTasks.any { task -> task.path == ":app:assembleRelease" || diff --git a/android/app/src/main/cpp/openstream_srt.cpp b/android/app/src/main/cpp/openstream_srt.cpp index da0db019..cdfa6085 100644 --- a/android/app/src/main/cpp/openstream_srt.cpp +++ b/android/app/src/main/cpp/openstream_srt.cpp @@ -33,6 +33,7 @@ namespace { constexpr const char *kTag = "shinSRT"; constexpr int kMinSrtLatencyMs = 80; constexpr int kMaxSrtLatencyMs = 200; +constexpr int kListenerAcceptPollMs = 100; constexpr int kMediaCodecBufferFlagKeyFrame = 1; constexpr int kMediaCodecBufferFlagCodecConfig = 2; constexpr int kAudioSampleRate = 48000; @@ -485,13 +486,16 @@ std::optional parseSrtUrl(const std::string &url) { class NativeSender { public: NativeSender() { + sendWorker_ = std::thread(&NativeSender::runSendWorker, this); + } + + void initializeRuntime() { #if OPENSTREAM_HAVE_LIBSRT srtStarted_ = srt_startup() == 0; if (!srtStarted_) { logError("libsrt startup failed"); } #endif - sendWorker_ = std::thread(&NativeSender::runSendWorker, this); } ~NativeSender() { @@ -629,6 +633,12 @@ class NativeSender { srt_setsockopt(listenerSocket, 0, SRTO_PEERIDLETIMEO, &peerIdleTimeoutMs, sizeof peerIdleTimeoutMs); srt_setsockopt(listenerSocket, 0, SRTO_LATENCY, &latency, sizeof latency); srt_setsockopt(listenerSocket, 0, SRTO_PEERLATENCY, &latency, sizeof latency); + // Keep accept nonblocking. Cancellation is observed through a short epoll + // wait, and the thread that owns this listener closes it after leaving the + // wait. Closing it from disconnect() while srt_accept() is blocked can + // race libsrt's internal accept state and crash in CUDTUnited::accept. + int receiveSync = 0; + srt_setsockopt(listenerSocket, 0, SRTO_RCVSYN, &receiveSync, sizeof receiveSync); sockaddr_in address{}; address.sin_family = AF_INET; @@ -637,23 +647,72 @@ class NativeSender { if (srt_bind(listenerSocket, reinterpret_cast(&address), sizeof(address)) == SRT_ERROR) { __android_log_print(ANDROID_LOG_ERROR, kTag, "SRT bind failed: %s", srt_getlasterror_str()); - disconnect(); + closeListenerSocket(listenerSocket); return false; } if (srt_listen(listenerSocket, 1) == SRT_ERROR) { __android_log_print(ANDROID_LOG_ERROR, kTag, "SRT listen failed: %s", srt_getlasterror_str()); - disconnect(); + closeListenerSocket(listenerSocket); + return false; + } + + const int epoll = srt_epoll_create(); + if (epoll == SRT_ERROR) { + logError("Could not create SRT listener poller"); + closeListenerSocket(listenerSocket); + return false; + } + const int listenerEvents = SRT_EPOLL_IN; + if (srt_epoll_add_usock(epoll, listenerSocket, &listenerEvents) == SRT_ERROR) { + logError("Could not register SRT listener with poller"); + srt_epoll_release(epoll); + closeListenerSocket(listenerSocket); return false; } - sockaddr_storage peer{}; - int peer_len = sizeof(peer); __android_log_print(ANDROID_LOG_INFO, kTag, "Waiting for OBS caller on SRT port %s", parsed->port.c_str()); - const SRTSOCKET acceptedSocket = srt_accept(listenerSocket, reinterpret_cast(&peer), &peer_len); + SRTSOCKET acceptedSocket = SRT_INVALID_SOCK; + bool cancelled = false; + while (isLifecycleCurrent(expectedLifecycleGeneration)) { + SRT_EPOLL_EVENT event{}; + const int ready = srt_epoll_uwait(epoll, &event, 1, kListenerAcceptPollMs); + if (ready == 0) continue; + if (ready == SRT_ERROR) { + if (isLifecycleCurrent(expectedLifecycleGeneration)) { + __android_log_print(ANDROID_LOG_ERROR, kTag, "SRT listener poll failed: %s", srt_getlasterror_str()); + } else { + cancelled = true; + } + break; + } + if (!isLifecycleCurrent(expectedLifecycleGeneration)) { + cancelled = true; + break; + } + + sockaddr_storage peer{}; + int peer_len = sizeof(peer); + acceptedSocket = srt_accept(listenerSocket, reinterpret_cast(&peer), &peer_len); + if (acceptedSocket != SRT_INVALID_SOCK) break; + + int systemError = 0; + const int acceptError = srt_getlasterror(&systemError); + if (acceptError != SRT_EASYNCRCV) { + __android_log_print(ANDROID_LOG_ERROR, kTag, "SRT accept failed: %s", srt_getlasterror_str()); + break; + } + } + srt_epoll_remove_usock(epoll, listenerSocket); + srt_epoll_release(epoll); closeListenerSocket(listenerSocket); if (acceptedSocket == SRT_INVALID_SOCK) { - __android_log_print(ANDROID_LOG_ERROR, kTag, "SRT accept failed: %s", srt_getlasterror_str()); - disconnect(); + if (!cancelled && isLifecycleCurrent(expectedLifecycleGeneration)) { + __android_log_print(ANDROID_LOG_ERROR, kTag, "SRT accept failed: %s", srt_getlasterror_str()); + } + return false; + } + if (!isLifecycleCurrent(expectedLifecycleGeneration)) { + srt_close(acceptedSocket); return false; } srt_setsockopt(acceptedSocket, 0, SRTO_SNDTIMEO, &sendTimeoutMs, sizeof sendTimeoutMs); @@ -764,11 +823,7 @@ class NativeSender { sendQueueBytes_ = 0; } std::lock_guard ioLock(ioMutex_); - const SRTSOCKET listenerSocket = takeListenerSocket(); const SRTSOCKET socket = takeSocket(); - if (listenerSocket != SRT_INVALID_SOCK) { - srt_close(listenerSocket); - } if (socket != SRT_INVALID_SOCK) { srt_close(socket); } @@ -878,11 +933,9 @@ class NativeSender { return true; } - SRTSOCKET takeListenerSocket() { + bool isLifecycleCurrent(uint64_t expectedLifecycleGeneration) const { std::lock_guard lock(socketMutex_); - const SRTSOCKET socket = listener_socket_; - listener_socket_ = SRT_INVALID_SOCK; - return socket; + return lifecycleGeneration_ == expectedLifecycleGeneration; } void closeListenerSocket(SRTSOCKET socket) { @@ -932,8 +985,17 @@ StreamState g_state; } // namespace +// Android calls JNI_OnLoad only after the shared object's C++ global +// constructors have completed. libsrt's startup uses process-wide static +// registries, so starting it from NativeSender's global constructor can run +// before those registries are initialized and crash in PacketFilter setup. +extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *, void *) { + g_state.sender.initializeRuntime(); + return JNI_VERSION_1_6; +} + extern "C" JNIEXPORT void JNICALL -Java_dev_openstream_app_stream_SrtNativeBridge_beginSession( +Java_dev_openstream_app_stream_SrtNativeBridge_nativeBeginSession( JNIEnv *, jobject, jlong session_generation) { @@ -952,7 +1014,7 @@ Java_dev_openstream_app_stream_SrtNativeBridge_beginSession( } extern "C" JNIEXPORT jboolean JNICALL -Java_dev_openstream_app_stream_SrtNativeBridge_connect( +Java_dev_openstream_app_stream_SrtNativeBridge_nativeConnect( JNIEnv *env, jobject, jstring url, @@ -1003,7 +1065,7 @@ Java_dev_openstream_app_stream_SrtNativeBridge_connect( } extern "C" JNIEXPORT jboolean JNICALL -Java_dev_openstream_app_stream_SrtNativeBridge_listen( +Java_dev_openstream_app_stream_SrtNativeBridge_nativeListen( JNIEnv *env, jobject, jstring url, @@ -1054,7 +1116,7 @@ Java_dev_openstream_app_stream_SrtNativeBridge_listen( } extern "C" JNIEXPORT jboolean JNICALL -Java_dev_openstream_app_stream_SrtNativeBridge_sendVideo( +Java_dev_openstream_app_stream_SrtNativeBridge_nativeSendVideo( JNIEnv *env, jobject, jbyteArray data, @@ -1095,7 +1157,7 @@ Java_dev_openstream_app_stream_SrtNativeBridge_sendVideo( } extern "C" JNIEXPORT void JNICALL -Java_dev_openstream_app_stream_SrtNativeBridge_disconnect( +Java_dev_openstream_app_stream_SrtNativeBridge_nativeDisconnect( JNIEnv *, jobject, jlong session_generation) { const uint64_t generation = static_cast(session_generation); std::lock_guard lock(g_state.mediaMutex); @@ -1113,7 +1175,7 @@ Java_dev_openstream_app_stream_SrtNativeBridge_disconnect( } extern "C" JNIEXPORT jboolean JNICALL -Java_dev_openstream_app_stream_SrtNativeBridge_sendAudio( +Java_dev_openstream_app_stream_SrtNativeBridge_nativeSendAudio( JNIEnv *env, jobject, jbyteArray data, diff --git a/android/app/src/main/java/dev/openstream/app/LifecycleStartGate.kt b/android/app/src/main/java/dev/openstream/app/LifecycleStartGate.kt new file mode 100644 index 00000000..586de7ea --- /dev/null +++ b/android/app/src/main/java/dev/openstream/app/LifecycleStartGate.kt @@ -0,0 +1,29 @@ +package dev.openstream.app + +/** Prevents a restarted Activity from starting services while an earlier stop is tearing them down. */ +internal class LifecycleStartGate { + private var started = false + private var pendingTeardowns = 0 + + @Synchronized + fun onStart(): Boolean { + started = true + return pendingTeardowns == 0 + } + + @Synchronized + fun onStop() { + started = false + pendingTeardowns += 1 + } + + @Synchronized + fun finishTeardown(): Boolean { + check(pendingTeardowns > 0) { "No lifecycle teardown is pending" } + pendingTeardowns -= 1 + return started && pendingTeardowns == 0 + } + + @Synchronized + fun canStart(): Boolean = started && pendingTeardowns == 0 +} diff --git a/android/app/src/main/java/dev/openstream/app/MainActivity.kt b/android/app/src/main/java/dev/openstream/app/MainActivity.kt index 81e185de..5a80fd54 100644 --- a/android/app/src/main/java/dev/openstream/app/MainActivity.kt +++ b/android/app/src/main/java/dev/openstream/app/MainActivity.kt @@ -82,6 +82,7 @@ class MainActivity : Activity() { @Volatile private var pendingListenerStart = false @Volatile private var listenerGeneration = 0L @Volatile private var activityStarted = false + private val lifecycleStartGate = LifecycleStartGate() private var keepScreenOn = false private var displayOff = false private var originalBrightness = -1f @@ -231,11 +232,18 @@ class MainActivity : Activity() { override fun onStart() { super.onStart() activityStarted = true + if (!lifecycleStartGate.onStart()) return + startActivityServices() + } + + private fun startActivityServices() { + if (!activityStarted || !lifecycleStartGate.canStart()) return phoneAdvertiser.start() obsDiscoveryClient.start() controlServer.start() startPreviewIfAllowed() startPhoneServerIfAllowed() + startPendingConnectIfReady() } override fun onResume() { @@ -246,25 +254,35 @@ class MainActivity : Activity() { if (savedPort != currentPort && savedPort in 1024..65535) { changePort(savedPort) } - if (pendingConnectAfterSettings) { - pendingConnectAfterSettings = false - startStream(connectionTargetFromSettings()) - } + startPendingConnectIfReady() + } + + private fun startPendingConnectIfReady() { + if (!pendingConnectAfterSettings || !activityStarted || !lifecycleStartGate.canStart()) return + pendingConnectAfterSettings = false + startStream(connectionTargetFromSettings()) } override fun onStop() { activityStarted = false + lifecycleStartGate.onStop() cancelLensRestart() stopLiveDotAnimation() // Never block the UI thread on network/thread joins (AGENTS.md 6). // Flag flips are immediate; heavy teardown runs on a daemon thread. // stopPhoneServer detects a non-UI thread and runs blocking teardown inline. Thread({ - runCatching { camera.stop() } - runCatching { stopPhoneServer(clearReservation = false, updateStatus = false) } - runCatching { obsDiscoveryClient.stop() } - runCatching { phoneAdvertiser.stop() } - runCatching { controlServer.stop() } + try { + runCatching { camera.stop() } + runCatching { stopPhoneServer(clearReservation = false, updateStatus = false) } + runCatching { obsDiscoveryClient.stop() } + runCatching { phoneAdvertiser.stop() } + runCatching { controlServer.stop() } + } finally { + if (lifecycleStartGate.finishTeardown()) { + mainHandler.post { startActivityServices() } + } + } }, "shinActivityStop").apply { isDaemon = true; start() } super.onStop() } @@ -786,6 +804,7 @@ class MainActivity : Activity() { } private fun startPhoneServerIfAllowed() { + if (!activityStarted || !lifecycleStartGate.canStart()) return if (phoneServerRunning) return if (listenerThread?.isAlive == true) { pendingListenerStart = true @@ -1189,7 +1208,8 @@ class MainActivity : Activity() { // ─────────────────────────── Utilities ─────────────────────────── private fun startPreviewIfAllowed() { - if (checkSelfPermission(Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED && + if (activityStarted && lifecycleStartGate.canStart() && + checkSelfPermission(Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED && cameraPreview.holder.surface.isValid ) { camera.startPreview() diff --git a/android/app/src/main/java/dev/openstream/app/discovery/PersistentInstanceId.kt b/android/app/src/main/java/dev/openstream/app/discovery/PersistentInstanceId.kt new file mode 100644 index 00000000..516a9fb3 --- /dev/null +++ b/android/app/src/main/java/dev/openstream/app/discovery/PersistentInstanceId.kt @@ -0,0 +1,15 @@ +package dev.openstream.app.discovery + +/** Reuses a persisted identity or synchronously saves a newly generated one. */ +internal fun loadOrCreateInstanceId( + existingId: String?, + persist: (String) -> Boolean, + create: () -> String, +): String { + existingId?.trim()?.takeIf { it.isNotEmpty() }?.let { return it } + + val generatedId = create().trim() + require(generatedId.isNotEmpty()) { "Generated instance ID must not be blank" } + check(persist(generatedId)) { "Could not persist phone discovery instance ID" } + return generatedId +} diff --git a/android/app/src/main/java/dev/openstream/app/discovery/PhoneDiscoveryAdvertiser.kt b/android/app/src/main/java/dev/openstream/app/discovery/PhoneDiscoveryAdvertiser.kt index 7977c1ea..bdf17576 100644 --- a/android/app/src/main/java/dev/openstream/app/discovery/PhoneDiscoveryAdvertiser.kt +++ b/android/app/src/main/java/dev/openstream/app/discovery/PhoneDiscoveryAdvertiser.kt @@ -4,6 +4,7 @@ import android.content.Context import android.net.wifi.WifiManager import android.os.Build import android.text.format.Formatter +import android.util.Log import dev.openstream.app.control.CameraControlServer import dev.openstream.app.encoder.advertisedMimeType import dev.openstream.app.stream.StreamConfig @@ -26,7 +27,6 @@ class PhoneDiscoveryAdvertiser( private val running = AtomicBoolean(false) @Volatile private var pendingRestart = false @Volatile private var worker: Thread? = null - private val instanceId = UUID.randomUUID().toString() fun start() { if (running.get()) return @@ -55,20 +55,30 @@ class PhoneDiscoveryAdvertiser( } private fun run() { + val instanceId = runCatching { + val preferences = context.applicationContext.getSharedPreferences( + INSTANCE_PREFERENCES, + Context.MODE_PRIVATE, + ) + loadOrCreateInstanceId( + existingId = preferences.getString(INSTANCE_ID_KEY, null), + persist = { id -> preferences.edit().putString(INSTANCE_ID_KEY, id).commit() }, + create = { UUID.randomUUID().toString() }, + ) + }.getOrElse { error -> + Log.e(TAG, "Could not load persistent phone discovery identity", error) + finishWorkerAndRestart() + return + } val socket = runCatching { DatagramSocket().apply { broadcast = true } }.getOrElse { - running.set(false) - if (worker === Thread.currentThread()) worker = null - if (pendingRestart) { - pendingRestart = false - start() - } + finishWorkerAndRestart() return } try { while (running.get()) { - val bytes = beaconPayload().toByteArray(StandardCharsets.UTF_8) + val bytes = beaconPayload(instanceId).toByteArray(StandardCharsets.UTF_8) val destinations = linkedSetOf( InetAddress.getByName("255.255.255.255"), InetAddress.getByName(DISCOVERY_MULTICAST_ADDRESS), @@ -89,16 +99,20 @@ class PhoneDiscoveryAdvertiser( } } finally { socket.close() - if (worker === Thread.currentThread()) worker = null - running.set(false) - if (pendingRestart) { - pendingRestart = false - start() - } + finishWorkerAndRestart() + } + } + + private fun finishWorkerAndRestart() { + if (worker === Thread.currentThread()) worker = null + running.set(false) + if (pendingRestart) { + pendingRestart = false + start() } } - private fun beaconPayload(): String { + private fun beaconPayload(instanceId: String): String { val json = JSONObject() .put("type", TYPE) .put("version", 1) @@ -127,6 +141,9 @@ class PhoneDiscoveryAdvertiser( } companion object { + private const val TAG = "shinPhoneAdvertiser" + private const val INSTANCE_PREFERENCES = "shin_phone_discovery" + private const val INSTANCE_ID_KEY = "phone_instance_id" const val DISCOVERY_PORT = 51615 const val DISCOVERY_MULTICAST_ADDRESS = "239.255.43.99" const val PREFIX = "SHIN_PHONE/1" diff --git a/android/app/src/test/java/dev/openstream/app/LifecycleStartGateTest.kt b/android/app/src/test/java/dev/openstream/app/LifecycleStartGateTest.kt new file mode 100644 index 00000000..10cb5122 --- /dev/null +++ b/android/app/src/test/java/dev/openstream/app/LifecycleStartGateTest.kt @@ -0,0 +1,35 @@ +package dev.openstream.app + +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class LifecycleStartGateTest { + @Test + fun returningBeforeStopTeardownFinishesDefersRestartUntilCleanupCompletes() { + val gate = LifecycleStartGate() + + assertTrue(gate.onStart()) + gate.onStop() + assertFalse(gate.canStart()) + + assertFalse(gate.onStart()) + assertFalse(gate.canStart()) + assertTrue(gate.finishTeardown()) + assertTrue(gate.canStart()) + } + + @Test + fun staleTeardownCompletionCannotRestartAfterAnotherStop() { + val gate = LifecycleStartGate() + gate.onStart() + gate.onStop() + gate.onStart() + gate.onStop() + + assertFalse(gate.finishTeardown()) + assertFalse(gate.canStart()) + assertFalse(gate.finishTeardown()) + assertFalse(gate.canStart()) + } +} diff --git a/android/app/src/test/java/dev/openstream/app/discovery/PersistentInstanceIdTest.kt b/android/app/src/test/java/dev/openstream/app/discovery/PersistentInstanceIdTest.kt new file mode 100644 index 00000000..a842abfa --- /dev/null +++ b/android/app/src/test/java/dev/openstream/app/discovery/PersistentInstanceIdTest.kt @@ -0,0 +1,36 @@ +package dev.openstream.app.discovery + +import org.junit.Assert.assertEquals +import org.junit.Test + +class PersistentInstanceIdTest { + @Test + fun identityIsCommittedOnceAndReusedByTheNextAdvertiserRun() { + var persistedId: String? = null + var generatedCount = 0 + + val firstRunId = loadOrCreateInstanceId( + existingId = persistedId, + persist = { id -> persistedId = id; true }, + create = { generatedCount += 1; "phone-instance-1" }, + ) + val secondRunId = loadOrCreateInstanceId( + existingId = persistedId, + persist = { id -> persistedId = id; true }, + create = { generatedCount += 1; "phone-instance-2" }, + ) + + assertEquals("phone-instance-1", firstRunId) + assertEquals(firstRunId, secondRunId) + assertEquals(1, generatedCount) + } + + @Test(expected = IllegalStateException::class) + fun failedPersistencePreventsReturningAnUnstableIdentityForAdvertising() { + loadOrCreateInstanceId( + existingId = null, + persist = { false }, + create = { "phone-instance-1" }, + ) + } +} diff --git a/android/gradle.properties b/android/gradle.properties index e022458f..20cf9a84 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -2,5 +2,5 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 android.useAndroidX=true android.nonTransitiveRClass=true -openstream.versionName=1.0.1 -openstream.versionCode=1789925364 +openstream.versionName=1.0.2 +openstream.versionCode=1790441644 diff --git a/build_plugin.bat b/build_plugin.bat index 593a1939..86cb67df 100644 --- a/build_plugin.bat +++ b/build_plugin.bat @@ -29,7 +29,7 @@ set "OBS_BIN=" set "PACKAGE_DIR=" set "CMAKE_EXE=cmake" set "QT_ROOT=%OPENSTREAM_QT_ROOT%" -if not defined OPENSTREAM_VERSION set "OPENSTREAM_VERSION=1.0.1" +if not defined OPENSTREAM_VERSION set "OPENSTREAM_VERSION=1.0.2" if defined OPENSTREAM_OBS_INSTALL set "OBS_INSTALL=%OPENSTREAM_OBS_INSTALL%" if defined OPENSTREAM_PLUGIN_BUILD_DIR set "BUILD_DIR=%OPENSTREAM_PLUGIN_BUILD_DIR%" diff --git a/build_plugin_linux.sh b/build_plugin_linux.sh index 6df32aa2..093695b5 100755 --- a/build_plugin_linux.sh +++ b/build_plugin_linux.sh @@ -4,12 +4,10 @@ # # ./build_plugin_linux.sh [--package-only] [--install-user] [--install-system] # -# Defaults mirror build_plugin.bat: OPENSTREAM_VERSION defaults to 1.0.1 -# (keep in sync with obs-plugin/CMakeLists.txt, android/app/build.gradle.kts, -# and tools/installer/openstream-obs-plugin.iss). +# Defaults mirror build_plugin.bat. Keep the version in sync with +# android/gradle.properties, obs-plugin/CMakeLists.txt, and the installer. # -# Dependencies (do not install system packages from here; install them first, -# see docs/linux.md): +# Dependencies (install them before running this script): # cmake, g++, pkg-config, libobs-dev, libavformat-dev, libavcodec-dev, # libavutil-dev, libswscale-dev, qt6-base-dev set -euo pipefail @@ -18,7 +16,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PLUGIN_DIR="${SCRIPT_DIR}/obs-plugin" BUILD_DIR="${OPENSTREAM_PLUGIN_BUILD_DIR:-${PLUGIN_DIR}/build}" PACKAGE_DIR="${OPENSTREAM_PLUGIN_PACKAGE_DIR:-}" -OPENSTREAM_VERSION="${OPENSTREAM_VERSION:-1.0.1}" +OPENSTREAM_VERSION="${OPENSTREAM_VERSION:-1.0.2}" PACKAGE_ONLY=0 INSTALL_USER=0 @@ -45,7 +43,7 @@ echo "" for tool in cmake g++ pkg-config; do if ! command -v "${tool}" >/dev/null 2>&1; then - echo "ERROR: required tool '${tool}' was not found. See docs/linux.md." >&2 + echo "ERROR: required tool '${tool}' was not found." >&2 exit 1 fi done @@ -57,10 +55,10 @@ for module in libobs libavformat libavcodec libavutil libswscale Qt6Core Qt6Widg fi done if [[ "${#missing_pc[@]}" -gt 0 ]]; then - echo "ERROR: missing pkg-config modules: ${missing_pc[*]}. See docs/linux.md." >&2 + echo "ERROR: missing pkg-config modules: ${missing_pc[*]}." >&2 exit 1 fi -echo "OBS system ABI (no Windows version pin; see docs/linux.md):" +echo "OBS system ABI (no Windows version pin):" echo " libobs $(pkg-config --modversion libobs)" echo " OBS frontend API: resolved by CMake from the installed OBS development package" echo " libavformat $(pkg-config --modversion libavformat)" diff --git a/docs/agent-roadmap.md b/docs/agent-roadmap.md deleted file mode 100644 index 7e9171e4..00000000 --- a/docs/agent-roadmap.md +++ /dev/null @@ -1,174 +0,0 @@ -# OpenStream agent roadmap - -This roadmap turns the [product readiness review](product-readiness-review.md) into work that a coordinator can assign to subagents. It targets one Android phone supplying one native OBS camera source over a local network on Windows and native Linux. It does not assume that every issue in the review is a confirmed runtime defect. Measure uncertain behavior before changing it. - -The roadmap is complete when the **same release** of the Android app and both OBS plugins can be installed, updated, connected, controlled, disconnected, and reconnected on supported hardware; the exact artifacts pass the automated and physical gates below; and the user-facing names, docs, and downloads agree. A passing unit-test suite alone does not meet that definition. - -## How the coordinator runs the work - -1. Create one issue and one PR for each numbered package. Give each subagent its own branch or worktree. A subagent owns its files until its PR is reviewed; no two agents edit `MainActivity.kt`, `openstream-source.cpp`, or release workflows at the same time. -2. Start each package from the latest accepted predecessor. Record the old behavior, make the smallest change, run its checks, and attach the evidence to the PR. Keep Android, OBS, and `docs/protocol.md` in one PR when a wire behavior changes. -3. Require a reviewer to inspect the actual diff and test output. Use **VERIFIED**, **NOT VERIFIED**, or **INCONCLUSIVE** for each acceptance check. An unrun physical check is INCONCLUSIVE, even when CI passes. -4. Keep `applicationId=dev.openstream.app` and the published Android signing identity. Preserve source timestamps, bounded queues, hardware-encode policy, and old scene/protocol identities that real prior artifacts used. Do not introduce a second phone encoder, silent software video fallback, or network work on the UI thread. -5. Do not publish a release from an open PR. The coordinator collects the exact APK and plugin hashes, checks the four Android/plugin version combinations, and obtains the physical acceptance record before tagging and publishing. - -Each subagent's handoff must name its package ID, PR, changed files, Android-to-OBS workflow step affected, old/new compatibility result, commands and device tests actually run, measured before/after results for performance work, and remaining INCONCLUSIVE checks. The coordinator updates [docs/testing.md](testing.md) only with observed results. - -## Work order and parallel lanes - -| Stage | Coordinator's gate | Work that can run in parallel after the gate | -| --- | --- | --- | -| 0. Establish the baseline | B1 records the published artifacts; B2 then records candidate device behavior | After B1, V1, O1, and A1 can use separate worktrees while B2 runs. | -| 1. Make builds and identity truthful | V1 and V2 prove matching artifact versions; O1 proves old scene behavior | A2 follows A1. V3 follows V2. | -| 2. Make the session reliable | A3 follows A2; the session owner and Stop behavior pass device checks | D1 can proceed separately. P1 follows A3 and O1. | -| 3. Finish both ends of the connection | P1 and P2 pass protocol and discovery checks; O2 provides host control parity | Run P1, P2, O2, then M1 in sequence where they share source files. U1 and U2 follow the state and control changes. | -| 4. Prove a releasable product | G1 checks exact artifacts, compatibility, device matrix, and new-user trials | Optional performance or transport work begins only from measured demand. | - -The coordinator can change this order when baseline evidence shows a more urgent defect. Write the reason and the replacement acceptance check in the relevant PR. Keep protocol changes together across Android and OBS, even when subagents investigate the two sides separately. - -## Stage 0: establish what works now - -### B1. Record the published release and candidate baseline - -- **Agent owns:** `docs/testing.md`, a reproducible evidence script or checklist under `tools/`, and saved artifact metadata. Download the published v1.0.1 APK and Windows plugin. Read the APK's real package ID, version code, and signing fingerprint. Save the asset hashes and OBS scene/source IDs from an actual old scene collection. Record the current candidate's APK/plugin hashes separately. -- **Prove:** Install the candidate APK over the published APK on a phone with the same signing identity. Open an old scene in real OBS and note whether the source loads, retains settings, and receives media. Record the result as VERIFIED or INCONCLUSIVE, not as assumed compatibility. -- **Blocker:** This needs a phone, OBS, the published artifacts, and release signing access. Other code packages may proceed, but G1 cannot pass without it. - -### B2. Build the repeatable end-to-end check - -- **Agent owns:** `docs/testing.md` and a small runner or recording template under `tools/`. Specify device model/Android version, OBS version/host OS, APK/plugin hashes, Wi-Fi setup, latency setting, result, and log location. -- **Prove:** Exercise install, first discovery, reserve, first video and AAC audio, lens/torch/zoom, Stop, reconnect after short and long loss, OBS restart, source removal, Virtual Camera, and a 30-minute run. Include a clap or tone for A/V sync. Record latency, dropped media, recovery time, battery, and temperature. -- **Gate:** At least three distinct Android devices and both supported OBS hosts run the same script before G1. A device or host not run is explicitly unsupported or unverified for that release. -- **Depends on:** B1, because both packages update `docs/testing.md`. On each host, start OBS from a clean installation, confirm one plugin loads and its source and controls appear, then test OBS shutdown with the phone unreachable. Keep an unrelated OBS source active while OpenStream fails to check isolation. - -## Stage 1: one version and honest OBS identity - -### V1. Move the candidate version to one source - -- **Agent owns:** new `release/version.properties` with `productVersion` and `androidVersionCode`; `android/app/build.gradle.kts`; `obs-plugin/CMakeLists.txt`; `build_plugin.bat`; `build_plugin_linux.sh`; `tools/installer/openstream-obs-plugin.iss`; the Windows and Linux CI workflows; and version tests. -- **Change:** Parse the root values in Gradle and CMake. Make build scripts pass the same product version to CMake and Inno. Remove independent `1.0.1` defaults and the literal `1.0.1` assertions in `tests/test_repo_contract.py`. Give private `website/package.json` a tooling version, not a public product claim. -- **Prove:** A deliberate mismatch fails a pull-request build. A normal build reports one candidate product version on Android, Windows, and Linux. Keep `SHIN/1`, saved OBS source IDs, OBS 32.2.1/FFmpeg ABI, and vendored SRT versions separate. Do not lower the current Android version code or change signing. -- **Depends on:** B1's actual published APK metadata. The next release code must exceed every published APK code for the same application ID, including prereleases, and the current candidate code. - -### V2. Check the binaries, installer, and release tag - -- **Agent owns:** a version checker under `tools/`, `.github/workflows/release.yml`, plugin version metadata in `obs-plugin/src/openstream-source.cpp`, package scripts, and a generated release manifest. -- **Change:** Embed an inspectable product version in the plugin. Have CI compare the tag, APK package/name/code/signing certificate, both plugin versions, Windows installer metadata, artifact hashes, and manifest. Check the previous **published APK's** code and certificate; do not infer them only from historical tag timestamps. Fail closed when fetching the previous release fails. Reject a stale binary in Linux `--package-only` mode. Split candidate build from publication: current `release.yml` builds and calls `gh release create` automatically on a tag push, before any physical acceptance gate. -- **Prove:** CI rejects a wrong tag, wrong APK version code, wrong signing certificate, stale plugin binary, wrong installer version, and missing artifact. It accepts a matched candidate. The checker runs on PRs where possible and again on the exact release artifacts. Candidate workflow uploads signed artifacts without publishing; a separate manual publish step consumes those exact bytes and their hashes. -- **Depends on:** V1. The release manifest names protocol and supported OBS ABI separately from product version. - -### V3. Separate development version from published download version - -- **Agent owns:** `website/src/main.jsx`, `website/index.html`, `website/package.json` and lockfile, `README.md`, `docs/release.md`, and `docs/release-notes-template.md`. -- **Change:** Remove hard-coded current-version claims from pages that link to `releases/latest`. Show “Latest release” until the deployment has the published release manifest, then show that manifest's version and matched assets. Keep historical notes fixed. Generate new release notes for each release instead of overwriting v1.0.1 history. -- **Prove:** A candidate bump cannot make the public site advertise an unpublished APK. A published release changes the site only after matching assets exist. Website build and link checks pass. -- **Depends on:** V2's manifest. This package does not change the Android application ID or release signing. - -### O1. Prove and repair OBS source identity - -- **Agent owns:** `obs-plugin/src/openstream-source.cpp`, native source tests, old scene fixtures, and setup docs. -- **Current evidence:** The plugin registers only `shin_phone_source` and displays `shin`. Current docs say users should add `OpenStream V8` and claim V7 saved-scene support. The source does not establish that claim. -- **Change:** Inspect source IDs in published plugins and saved scenes. Keep every confirmed old ID loadable through an adapter or explicit migration. Change only the new source's visible picker name to `OpenStream Camera`; retain its stable internal ID. Correct docs to match what OBS actually shows. -- **Prove:** A published-release scene opens in Windows and Linux OBS without a missing-source dialog, keeps settings, and receives media. A new scene displays `OpenStream Camera`. If an old ID never existed in an artifact, document that rather than inventing an adapter. -- **Depends on:** B1. Do this before changing UI labels or promising scene compatibility. - -## Stage 2: make the Android session reliable - -### A1. Define the session states and Stop behavior - -- **Agent owns:** `android/app/src/main/java/dev/openstream/app/ReservationState.kt`, a new pure session-state module, `MainActivity.kt` call sites, and JVM behavior tests. -- **Change:** Model Available, Selected, Reserved, Connecting, Live, Reconnecting, Error, and Stopped with explicit transition events and a generation for stale work. Decide and implement distinct actions: **Stop** ends media and does not auto-restart; **Start** makes the phone available again; **Disconnect** releases the OBS reservation while the phone remains available. Today the Stop button sets `pendingListenerStart`, so its label and behavior disagree. -- **Prove:** Unit tests cover duplicate reserves, rapid phone switching, timeout, stale callback, reconnect, release, Stop, and Start. On a device, Stop remains stopped across background/foreground, and a visible Start action returns to reserve/live without restarting the app. The phone and OBS show the same transition. Avoid spreading state flags across additional UI callbacks. - -### A2. Remove blocking teardown from Android UI paths - -- **Agent owns:** `MainActivity.kt`, `camera/Camera2Controller.kt`, `stream/SrtStreamClient.kt`, encoders, and native SRT adapter as needed. -- **Change:** Move camera/codec/socket start and stop to one serialized, bounded session worker. Keep view changes on the main thread. `surfaceDestroyed()` currently calls camera and server teardown from the UI callback; other UI paths can reach native disconnect and codec stop. Ensure each operation checks session generation so late work cannot revive a stopped stream. -- **Prove:** Android behavior tests cover rapid start/stop, lens change, Settings, preview destruction, and stale callbacks. StrictMode or trace evidence on real phones shows no network/disk work or long codec teardown on the UI thread. Each queue or worker declares its capacity and overflow behavior. -- **Depends on:** A1. Do not parallel-edit `MainActivity.kt` with A3. - -### A3. Give live media a lifecycle owner - -- **Agent owns:** a foreground camera session module or equivalent, `MainActivity.kt`, `AndroidManifest.xml`, preview binding, notification actions, and Android tests. -- **Change:** The session owner holds camera, encoders, SRT, discovery, control, and reservation state. The activity observes state and supplies a preview surface. Define behavior for Home, screen lock, Settings, permission revoke, task removal, and process recreation under supported Android versions. Keep one video encoder and make notification Stop final. -- **Prove:** A device run shows the documented background behavior without a stale reservation or second encoder. A fast return to the app restores preview and current state. Behavioral tests exercise owner transitions; instrumentation covers lifecycle and permission edges where an emulator can reproduce them. -- **Depends on:** A2. Android background-camera restrictions require device checks before promising uninterrupted screen-off streaming. - -## Stage 3: secure and finish the connection - -### P1. Make pairing and control authorization one protocol change - -- **Agent owns:** Android `control/CameraControlServer.kt`, discovery and reservation code, OBS `openstream-source.cpp` control client, `docs/protocol.md`, and cross-product tests. One integration PR owns both ends and the wire documentation. -- **Current evidence:** V1 control is not authenticated. `/reserve` can receive an optional token; camera commands rely on the reserved peer address. The docs claim a pairing URL carries a control token, but the current URL builder does not add one. -- **Change:** Specify capability/version negotiation, phone approval, a session secret, expiry and rotation, and authorization for every mutating command. Define a safe response for previous Android/new OBS and new Android/previous OBS. Do not silently accept an incompatible pairing or expose secrets in discovery, logs, or a support bundle. -- **Prove:** Another LAN host cannot reserve or control the camera; stale and released secrets fail. Test reserve, control, release, reconnect, and both previous/candidate combinations on actual Android and OBS. Keep source timestamps and SRT media unchanged unless a separate measured media change is needed. -- **Depends on:** A3 and O1. The coordinator owns this shared protocol PR; separate subagents may investigate each side without concurrent edits to the same branch. - -### P2. Repair discovery and deep-link fallback - -- **Agent owns:** Android `discovery/*.kt`, `stream/ConnectionTarget.kt`, `MainActivity.kt`, `AndroidManifest.xml`, OBS pairing URL code, protocol docs, and parsing tests. -- **Current evidence:** The manifest advertises `openstream://connect`, but the manual `ConnectionTarget.fromPairingUri()` path accepts only `shin://connect`. Confirm behavior for published links before modifying it. -- **Change:** Parse confirmed legacy and current schemes, validate host/port/latency and input sizes, and show a specific reason when multicast, Wi-Fi, VPN, firewall, or port binding prevents discovery. Keep manual connection as a reachable fallback. -- **Prove:** Old and new links connect on a device; malformed links are rejected without a crash. Discovery and manual pairing work on the same network and on a multicast-blocked network. Review pairing behavior against P1's version rules. -- **Depends on:** P1's protocol specification. If the parser fix is independent, it may be a smaller earlier PR after B1. - -### O2. Give Windows and Linux equivalent live controls - -- **Agent owns:** `obs-plugin/CMakeLists.txt`, `src/openstream-dock.cpp`, `src/openstream-source.cpp`, OBS tests, and setup docs. -- **Current evidence:** The dock builds only on Linux, while Windows docs tell users to open it. Linux source properties and the dock also overlap on some controls. -- **Change:** Make a dock or equally clear live control area available on both supported hosts. Keep setup settings in source properties; put live connect/release, selection, status, zoom, lens, torch, and identify actions in one place per host. Remove a duplicate control only after its replacement works on Windows and Linux. -- **Prove:** Real OBS on both hosts shows each action, the action reaches the selected phone, and an unreachable phone cannot freeze OBS. The old saved scene still opens. -- **Depends on:** O1, V1, and P1. Apply its protocol changes before editing the same OBS source file for live controls. - -### M1. Make media failures observable and test the receiver behavior - -- **Agent owns:** Android `encoder/MediaCodecAudioEncoder.kt`, video encoder, `stream/SrtStreamClient.kt`, `cpp/openstream_srt.cpp`; OBS `media-clock.hpp`, `async-control-client.*`, `openstream-source.cpp`; and behavioral media tests. -- **Change:** Surface video-only, audio muted/failed, encoder unavailable, software AAC fallback, SRT queue saturation, and reconnect reasons in the app and OBS. Keep phone software video encode disabled unless the user explicitly opts into it. Preserve source presentation timestamps and log recording gaps. Add executable tests for packet timestamps, loss, bounded queues, stop/release, and reconnect rather than relying only on Python source-text checks. -- **Prove:** A failed microphone does not silently look healthy; loss and overflow produce a visible state and bounded recovery. A/V offset stays measured after reconnect and Virtual Camera starts. Every performance adjustment includes same-device before/after CPU, latency, drops, battery, and heat. -- **Depends on:** A3, P1, and O2. Treat hardware OBS decode as an experiment after the baseline: the current probe always falls back to software. Ship a hardware path only if measured benefit justifies it and the fallback remains explicit. - -### U1. Make setup and labels match the real workflow - -- **Agent owns:** Android `strings.xml`, layouts, `MainActivity.kt`, `SettingsActivity.kt`; OBS visible labels and docs; new-user acceptance script. -- **Change:** Show OpenStream consistently while keeping confirmed legacy IDs and stored preference keys. Guide permission grant, OBS discovery, selection, pairing, first frame, and audio check. Put raw IP, ports, and latency in Advanced. Show version mismatch and offer a matched download. Make Stop and Disconnect distinct and discoverable. -- **Prove:** A person who has never built the repo reaches live video and audio without reading protocol docs, can identify the connected phone, and can stop or recover deliberately. Check labels and accessibility on a real phone and both OBS hosts. -- **Depends on:** A1, A3, O1, O2, P1, and P2. Do not use a broad visual redesign to substitute for connection clarity. - -### U2. Add actionable diagnostics and a feedback path - -- **Agent owns:** Android and OBS status presentation, bounded recent-event logs, a redacted support export, `docs/set-up.md`, and issue templates. -- **Change:** Report discovery, control, reservation, SRT media, audio, and encoder health separately. Give a next action for denied permission, wrong network, phone busy, firewall/port issue, codec failure, and stale reservation. Export exact app/plugin/OBS/OS versions, state transitions, and error codes only with user action; remove secrets, IP addresses, and raw media. -- **Prove:** Each injected failure maps to one clear message on the phone and OBS, and a tester can submit a useful report without a developer extracting Logcat. Validate redaction with tests and inspect an actual export. -- **Depends on:** P1, M1, and U1. - -### D1. Add the public trust and support basics - -- **Agent owns:** root license/provenance inventory, `SECURITY.md`, `CONTRIBUTING.md`, privacy/support docs, issue templates, and release notes process. -- **Change:** Identify copyright holders and bundled dependency obligations. The project owner chooses the top-level license after reviewing that inventory; then add the approved license. Publish supported host/device limits, a security contact, update instructions, and a feedback form. Do not call the repository open-source before the license exists. -- **Prove:** A new contributor can build and report an issue; a user can find the supported platforms and privacy behavior from the download page. After candidate staging in G1, run 10 to 20 matched-artifact beta installs with people outside the developer workflow. Record setup completion, time to first frame, failed reconnects, and common support questions. -- **Depends on:** B1 for true release claims. It can run in parallel with technical packages except where it edits their docs. - -## Stage 4: release gate - -### G1. Publish only a matched, exercised release - -- **Agent owns:** release workflow, `docs/release.md`, final evidence record, and release manifest. The coordinator is the single writer of the final release decision. -- **Automated gate:** Repository tests, Android JVM tests/lint/native-SRT APK, Android instrumentation where practical, Windows and Linux plugin builds/native tests, installer/package smoke tests, website build, version checker, signature/code continuity, and hashes must be VERIFIED. CI's Python contracts supplement native tests; they do not replace them. -- **Candidate staging:** Build one signed APK and both host packages from a fixed commit in a workflow that uploads candidate artifacts without publishing. Pin the commit and record each artifact hash in the manifest. The current tag workflow auto-publishes, so remove that behavior before this gate is used. -- **Physical gate:** Download and install those exact bytes on at least three Android devices and both supported OBS hosts. Start real OBS and check clean module load, one source picker entry, and the expected controls before media testing. Complete B2's 30-minute, network-loss, A/V sync, Virtual Camera, controls, screen lock, old scene, OBS shutdown, unrelated-source isolation, and reinstall/update checks. Test previous/previous, previous/candidate, candidate/previous, and candidate/candidate Android/plugin combinations. Record actual results and release blockers. A missing device result stays INCONCLUSIVE and blocks claims of that device's support. -- **Publication:** After sign-off, a manual publish workflow verifies the staged hashes, tags the tested commit, and attaches the **same files** and release notes. It does not rebuild. The published SHA-256 hashes must equal the tested manifest. A changed artifact requires repeat verification. Then update the website's published version. The release notes state tested hosts and known limits. Do not treat an automatic CI green result as the entire product sign-off. - -## Add after the core release is proven - -- Add a simple Stable 720p and Balanced 1080p profile selector when device measurements show both are sustainable. Show the actual resolution, frame rate, bitrate, latency target, and thermal warning. -- Add USB only if beta reports show Wi-Fi setup or stability is a frequent blocker and a prototype shows better results without making installation harder. -- Add an optional, privacy-safe feedback channel and a public compatibility matrix from completed device runs. -- Consider hardware decode, more simultaneous phones, macOS, remote contribution, and advanced camera controls only with a measured use case and a test plan for both ends. - -## Remove, hide, or stop claiming - -- Remove hard-coded current product versions from build defaults, tests, and public download copy. Keep historical release notes and independent protocol/ABI numbers. -- Remove `shin` from visible new-user labels after confirming and preserving the internal IDs that old artifacts require. -- Remove duplicate OBS live controls only after Windows and Linux have the same working replacement. Hide manual network fields in Advanced, not in protocol or developer docs. -- Stop calling unmeasured reconnect behavior “reliable” or promising a latency number without device evidence. Stop claiming V7/V8 scene support until old scene fixtures load in real OBS. -- Keep the Python receiver as a developer tool. Do not add cloud accounts, built-in recording, filters, automatic microphone mixing, or a second phone encoder to the supported one-phone path. diff --git a/docs/architecture.md b/docs/architecture.md deleted file mode 100644 index 3a2668ed..00000000 --- a/docs/architecture.md +++ /dev/null @@ -1,114 +0,0 @@ -# OpenStream Architecture - -## V1 Pipeline - -```text -Android Camera2 - -> MediaCodec hardware AVC/H.264 video encode - -> MediaCodec AAC audio encode - -> MPEG-TS muxer (video + audio) - -> SRT caller over local Wi-Fi - -> Native OBS source plugin FFmpeg/libsrt listener - -> OBS video output (I420/NV12/BGRA) + audio output (separate mixer channel) - -> OBS final stream/record encode -``` - -The V1 path uses hardware video and audio encoding on the phone. AVC/H.264 at -1080p30 is the sustainable default; the app does not silently switch video to -software encoding when a hardware surface encoder is unavailable. - -USB transport is intentionally outside this release. The maintained path is -the legacy Android app and OBS source/dock over local Wi-Fi and SRT. - -## Discovery & Connection - -OBS starts the media listener and advertises it on the LAN with OpenStream UDP -multicast discovery. Android shows discovered OBS listeners as tappable devices, -then connects directly over SRT. - -The discovery flow: - -1. OBS plugin broadcasts UDP beacon every 1 second on port `51615` (multicast `239.255.43.99` + subnet broadcast). -2. Android app listens for beacons and displays discovered OBS instances. -3. User taps an OBS device -> Android constructs `srt://:?mode=caller&latency=`. -4. Android MPEG-TS stream begins. -5. OBS plugin uses FFmpeg to open the SRT input, decode video and audio, and output frames. - -Fallback: If UDP discovery is blocked, the OBS source exposes an -`openstream://connect?host=&port=&latency=&name=...` -URL for manual/QR pairing. - -## Remote Camera Controls - -The Android app runs a lightweight HTTP control server on port `9101`. The OBS -plugin sends POST requests to control the phone camera in real-time: - -| Endpoint | Body | Effect | -|----------|------|--------| -| `POST /zoom` | `{"value": 2.5}` | Set digital zoom level | -| `POST /torch` | `{"enabled": true}` | Toggle flashlight | -| `POST /lens` | `{"lens": "1×"}` | Switch camera lens | - -Control commands are separate from the media stream and use standard HTTP for -simplicity and debuggability. - -## Why not raw YUV first? - -Raw 1080p YUV420 is hundreds of megabits per second before overhead. Lossless -zstd compression is content-dependent and cannot be assumed to reach streaming -bitrates. The V1 system keeps a clean transport interface so a later raw/lossless -transport can be tested without blocking the reliable product path. - -## Transport Defaults - -Canonical values live in [protocol.md](protocol.md): SRT `:9100`, control `:9101`, discovery UDP `:51615`, `12 Mbps`, `120 ms`. - -## Encoding Defaults - -- Default video codec: AVC/H.264. -- HEVC/H.265 remains an explicit device/profile option only; it is not forced by the default path. -- Audio codec: AAC. -- Default resolution: `1920x1080`. -- Fallback resolution: `1280x720`. -- Default frame rate: `30 fps`. -- Bitrate range: `8-50 Mbps`; the default path uses `12 Mbps`. -- Keyframe interval: `1 second`. - -## Video Output Path - -The OBS plugin handles three pixel format paths to minimize unnecessary -conversions: - -1. **I420 (YUV420P)** - Zero-copy pass-through with proper color matrix. -2. **NV12** - Zero-copy pass-through with proper color matrix. -3. **BGRA fallback** - swscale conversion for non-standard decoder output. - -Color metadata (BT.601/BT.709/BT.2020, full/limited range, HDR TRC) is -extracted from FFmpeg frame properties and forwarded to OBS. - -## Sync Model - -V1 does not promise PTP or genlock. Each Android frame/audio buffer is -timestamped with a monotonic clock. The Windows receiver maps the source PTS -into one OBS monotonic session clock, preserving the audio/video offset instead -of replacing it with receiver-arrival time. Frames that arrive more than -`250 ms` late are dropped and logged so virtual-camera load cannot create an -unbounded backlog. - -## Telemetry - -The Android app publishes: - -- Device name -- Codec -- Resolution and frame rate -- Current bitrate -- Dropped frame estimate -- Battery percentage -- Temperature -- Wi-Fi RSSI -- Encoder state -- SRT target URL - -Telemetry is intentionally separate from the media path so OBS property -panels can evolve independently. diff --git a/docs/assets/setup/android-connect.svg b/docs/assets/setup/android-connect.svg deleted file mode 100644 index ba0aafc6..00000000 --- a/docs/assets/setup/android-connect.svg +++ /dev/null @@ -1,17 +0,0 @@ - - OpenStream Android connect screenshot - A screenshot-style Android app view showing an available OBS camera slot that can be tapped to connect. - - - - - Camera preview - Available OBS slots - - Main CAM · Available - - Tap to connect - - - Camera appears in OBS - diff --git a/docs/assets/setup/obs-source.svg b/docs/assets/setup/obs-source.svg deleted file mode 100644 index 7535f827..00000000 --- a/docs/assets/setup/obs-source.svg +++ /dev/null @@ -1,20 +0,0 @@ - - OBS OpenStream source screenshot - A screenshot-style OBS window showing an OpenStream camera-slot card in the persistent Cameras dock. - - - - OpenStream waits for phone - - OpenStream Camera Control - - Main CAM - Waiting for phone - - Connect - - + - Add source, then use its camera card - - Auto discovery ready · Manual setup: Advanced - diff --git a/docs/assets/setup/release-downloads.svg b/docs/assets/setup/release-downloads.svg deleted file mode 100644 index 4fa4472e..00000000 --- a/docs/assets/setup/release-downloads.svg +++ /dev/null @@ -1,22 +0,0 @@ - - OpenStream release downloads screenshot - A screenshot-style release page highlighting the Android APK, Windows installer, and manual plugin zip. - - - - - - - github.com/YashasVM/OpenStream/releases - Latest OpenStream release - Download these three files from Assets. - - openstream-android.apk - Android phone - - openstream-obs-plugin-installer-windows-x64.exe - Recommended - - openstream-obs-windows-x64.zip - Manual fallback - diff --git a/docs/assets/setup/windows-installer.svg b/docs/assets/setup/windows-installer.svg deleted file mode 100644 index 4c877117..00000000 --- a/docs/assets/setup/windows-installer.svg +++ /dev/null @@ -1,18 +0,0 @@ - - OpenStream Windows installer screenshot - A screenshot-style installer wizard asking for the OBS Studio install folder. - - - - OpenStream OBS Plugin Setup - Choose OBS Studio Folder - Install into the folder that contains bin\64bit\obs64.exe. - - C:\Program Files\obs-studio - - Next - - Back - - Restart OBS after the installer finishes. - diff --git a/docs/linux.md b/docs/linux.md deleted file mode 100644 index e354afe8..00000000 --- a/docs/linux.md +++ /dev/null @@ -1,137 +0,0 @@ -# OpenStream OBS plugin on Linux - -Native Linux support for the OpenStream OBS source plugin: build, install, -and validation notes for this branch. - -## Scope - -- Native (distro or PPA) OBS Studio on Linux x86_64 with Qt6. -- One OBS source reserves one phone. Reuse that source across scenes with Add - Existing; a phone cannot be reserved by two sources at once. -- **No Flatpak/Snap guarantee.** Sandboxed OBS builds isolate the plugin - directory and network namespace, so the install paths and discovery - behaviour below are not validated for them. - -## Dependencies - -Install the development packages first (example for Debian/Ubuntu; the -build script never installs system packages itself): - -- `cmake` (>= 3.24), `g++` (C++20), `pkg-config`, `ninja` (optional) -- `libobs-dev` (>= 30; provides the OBS core and frontend API development - files) -- `libavformat-dev`, `libavcodec-dev`, `libavutil-dev`, `libswscale-dev` -- `qt6-base-dev` (>= 6.2, for Qt6 Core and Widgets) - -Notes: - -- The native Linux module registers the **OpenStream Camera Control** dock in - OBS. It lists discovered phones, lets you name the source, connect or release - a reservation, and send zoom, lens, torch, and identify commands. These UI - dependencies stay in the OBS plugin target; the media engine does not link - OBS frontend or Qt Widgets. -- `libobs-dev` supplies the frontend API headers and link library on Ubuntu - 24.04, but does not ship an `obs-frontend-api.pc` pkg-config file. CMake - resolves the library through the system linker paths; the build script - checks the OBS core and Qt dependencies before configuring. -- Linux links the **system OBS/FFmpeg ABI** (whatever the distro OBS was - built against). There is intentionally no FFmpeg version pin on this path, - unlike the Windows release build, which stays pinned to OBS 32.2.1 / - avformat-62 (see `obs-plugin/CMakeLists.txt`). -- Reference configurations: local validation used libobs 32.2.2, FFmpeg - (avformat 63), Qt 6.11.2; CI (`obs-plugin-linux.yml`) uses Ubuntu 24.04 - with libobs-dev 30.0.2, FFmpeg 6.1.1, qt6-base-dev 6.4.2, and fails - visibly below libobs 30 / Qt 6.2. - -## Build - -```sh -./build_plugin_linux.sh -``` - -This configures incrementally (`obs-plugin/build` by default, override with -`OPENSTREAM_PLUGIN_BUILD_DIR`), builds `openstream-obs.so`, and runs the -C++ contract tests via `ctest`. Useful variants: - -- `OPENSTREAM_PLUGIN_BUILD_DIR=/tmp/openstream-linux-build ./build_plugin_linux.sh` - — validation build outside the repo. -- `OPENSTREAM_PLUGIN_PACKAGE_DIR="$PWD/artifacts" ./build_plugin_linux.sh` - — also stages `openstream-obs-linux-x86_64.tar.gz` (module + installer). -- `./build_plugin_linux.sh --package-only` — repackage an existing build. -- `./build_plugin_linux.sh --install-user` / `--install-system` — build, - test, then install (see below). - -## Install / uninstall - -Per-user install (default, no root). Respects `XDG_CONFIG_HOME`: - -```sh -./tools/installer/install-openstream-plugin-linux.sh -# installs to ${XDG_CONFIG_HOME:-$HOME/.config}/obs-studio/plugins/openstream-obs/bin/64bit/openstream-obs.so -``` - -System-wide install (requires root; the OBS system plugin dir is -auto-detected, e.g. `/usr/lib/obs-plugins`): - -```sh -sudo ./tools/installer/install-openstream-plugin-linux.sh --system -``` - -Testing hook (touches nothing outside the given directory): - -```sh -./tools/installer/install-openstream-plugin-linux.sh --dest-dir /tmp/os-install-test -``` - -The install is atomic (stage to a temp file in the destination, then -rename), replaces the canonical module, and removes only other-named stale -copies (`shin-obs.so`, `openstream-beta-obs.so`, `libopenstream-obs.so`, and -the legacy flat per-user copy) after the new copy succeeds. A per-user -installation also removes the old Shin module from its former plugin folder. - -Uninstall: delete `openstream-obs.so` from the user or system plugin -directory above and restart OBS. There is no uninstaller script. - -## Behavioural notes - -- Software decode warning: the receiver probes for a hardware decode - device but has no validated zero-copy hardware frame path, so decoding - always falls back to software with an explicit OBS log warning. Expect - corresponding CPU load on the OBS machine; this is stated, not measured, - here. -- Reliability: the camera-control TCP channel suppresses SIGPIPE per send - (and per socket where supported), so a disconnected phone fails the - command with an error instead of terminating OBS. Covered by - `obs-plugin/tests/test_sigpipe.cpp`, which reproduces the disconnected- - peer crash with flags 0 and proves the production flags survive with - EPIPE. -- Source timestamps are preserved end to end (MediaClock maps the phone - clock into OBS time; gaps are surfaced and logged, never replaced with - arrival time). Every receiver queue declares its capacity and overflow - policy. - -## Exact validation limits - -What was actually checked for this branch (local machine): - -- Direct `g++` C++20 syntax checks passed for the source, control client, and - dock against the installed OBS 32.2.2, FFmpeg, and Qt6 development headers. -- A manual shared-library link produced `openstream-obs.so`. `ldd` confirmed links - to `libobs`, `libobs-frontend-api`, FFmpeg, Qt6 Core, and Qt6 Widgets, with no - missing libraries. The dock registration symbols are present in the module. -- The Linux installer smoke test copied the linked module into an isolated - `--dest-dir`; the installed file matched the built module byte for byte. -- Focused Python contracts passed (4 tests), and `bash -n` passed for the - Linux build and installer scripts. -- CMake and CTest were not run because CMake is unavailable in this environment. -- See [reliability audit](reliability-audit.md) for the final check results. - -What was NOT validated and remains a blocker for release claims: - -- Loading `openstream-obs.so` inside a running native OBS GUI on Linux, including - dock behavior. -- A physical phone streaming over Wi-Fi (discovery, reservation, SRT - media, reconnect, controls). -- Any Flatpak/Snap OBS build, any distro other than the validation - machine, or physical camera sessions. -- No performance measurements were taken; no latency/CPU claims are made. diff --git a/docs/product-readiness-review.md b/docs/product-readiness-review.md deleted file mode 100644 index 674a0d1f..00000000 --- a/docs/product-readiness-review.md +++ /dev/null @@ -1,109 +0,0 @@ -# OpenStream product readiness review - -Reviewed on 2026-09-23 against the current `fix/openstream-prod-reliability` candidate. This is a source and build review, not a device acceptance result. The published v1.0.1 downloads predate the candidate changes. - -The ordered implementation plan is in the [agent roadmap](agent-roadmap.md). - -## Assessment - -**Confidence that this candidate is ready for broad, professional use: 5/10.** This is a judgment about release evidence and user experience, not a measured failure rate. The core Android-to-OBS design is credible and the product reportedly works, but a new user still has several ways to get stuck without a clear recovery path. The most important candidate workflow has no recorded physical phone and OBS sign-off. - -| Area | Score | Evidence | -| --- | ---: | --- | -| Core media and protocol engineering | 7/10 | Hardware phone encode, bounded SRT send queue, source timestamp handling, reservation logic, reconnect status, Linux and Windows native builds. See `docs/architecture.md`, `docs/protocol.md`, and `docs/reliability-audit.md`. | -| Reliability proof | 4/10 | CI runs repository, Android, and plugin checks, but `docs/reliability-audit.md` records no candidate device run. `docs/testing.md` lists the missing 30-minute, loss, sync, and old/new compatibility checks. | -| First-use and daily operation | 4/10 | Discovery, manual connection, source controls, and setup docs exist. Android and the current OBS source still display `shin`, while setup docs instruct users to add `OpenStream V8`. The live control dock builds only on Linux. | -| Installation and release discipline | 6/10 | Android signing and version-code gates, Windows installer tests, Linux packaging, checksums, and CI are present. The published release differs from the current candidate and Linux support is still a source-built candidate. | -| Trust and user feedback | 3/10 | No top-level project license, contribution guide, security contact, privacy statement, issue templates, or recorded user acceptance results. The absence of open issues is not evidence of user satisfaction. | - -The score is weighted toward the core workflow and reliability proof. It should rise only after candidate builds pass repeatable device tests and people outside the development environment complete setup and recovery without help. - -## What already works well - -- The product has a coherent local path: Camera2, hardware AVC and AAC, MPEG-TS over SRT, and a native OBS source. It avoids a cloud account and a second phone encoder in the standard mode. -- The app and plugin have explicit ownership and reconnect behavior. The phone transport has a byte-bounded send queue; the receiver preserves source timestamps and logs late media gaps. -- CI covers Android build and lint, C++ tests, Windows packaging, Linux packaging, and release signing checks. Saved-scene compatibility still needs an actual old-scene load test. -- The repo has a setup guide and protocol documentation, which give future maintainers a base for field support. - -## Highest-impact gaps - -1. **Prove the complete candidate workflow on devices.** `docs/reliability-audit.md` explicitly lacks a physical Android-to-OBS run. Test install-over-release, discovery, pairing, 30-minute video and audio, screen lock/background behavior, lens and torch changes, short and long Wi-Fi loss, OBS restart, Virtual Camera, and source removal on Windows and native Linux. Record device, OS, OBS version, network, measured latency, A/V offset, dropped frames, heat, and recovery time. Test previous and candidate Android/plugin combinations before a protocol release. -2. **Give the media session an Android owner independent of the activity.** `MainActivity.kt` creates the camera, encoders, transport, discovery, and control server, then stops them in `onStop()`. `surfaceDestroyed()` also calls camera/server teardown from a UI callback, and the Stop button schedules the listener to start again. A foreground camera service or equivalent session owner should manage live state, a real Stop/Start action, and teardown off the UI thread; the activity should observe it. First reproduce background and screen-lock behavior on supported Android versions, then implement the lifecycle change with behavioral tests. -3. **Make pairing authorization real.** `docs/protocol.md` says protocol V1 does not authenticate the control channel. `CameraControlServer.kt` accepts an optional reservation token and authorizes camera commands by reserved peer address. Add an explicit pairing confirmation and session secret, validate every control command, and show the paired OBS identity on the phone. Roll out with a versioned Android/plugin compatibility plan so old clients fail with an understandable upgrade message. -4. **Make one connection model visible everywhere.** The Android launcher name and current OBS source picker say `shin`; the docs call the source `OpenStream V8`. The Linux dock is absent from the Windows build. Verify saved scenes from earlier releases in OBS before changing any source ID, then present `OpenStream Camera` consistently in the phone, OBS source, controls, installer, and docs. Use the same human-readable states: Available, Pairing, Connected, Live, Reconnecting, and Action needed. -5. **Make failure recovery a product feature.** Show whether discovery, control, and SRT media are reachable separately. Give a specific next action for denied permissions, wrong network, busy phone, blocked port, failed hardware encoder, missing audio, and stale reservation. Add a redacted support bundle with app/plugin versions, device/OBS/OS details, recent state changes, and error codes. Never include pairing secrets or raw video. -6. **Finish the public trust basics.** Choose and add a top-level license after checking ownership and bundled dependencies. Add a security reporting path, privacy and local-network data statement, supported-platform table, contribution guide, issue templates, and a changelog tied to actual artifacts. Do not call the repository open-source until the license is published. - -## Feature decisions - -| Priority | Include | Why and acceptance check | -| --- | --- | --- | -| Next release | A guided first connection that checks permissions, matching app/plugin versions, phone and OBS discovery, reservation, then first video and audio | A new user completes setup without reading protocol docs; each failure names one corrective action. | -| Next release | Persistent live session, explicit Stop, reconnect reason and progress, and visible audio status | Locking the screen or opening another app follows a documented behavior; a brief network loss returns to the same OBS source. | -| Next release | Diagnostic screen and support bundle | A tester can report a failure with device and error details without sending video or secrets. | -| Next release | Device and OBS compatibility matrix in CI/manual sign-off | At least a small, named set of Android vendors and current OBS versions passes the same scripted scenarios before publishing. | -| After reliability gate | Simple quality presets such as Stable 720p and Balanced 1080p, with actual bitrate, frame rate, and health shown | Profiles are validated on hardware and tell users when thermal or network conditions force a change. Measure latency and heat before shipping adaptive behavior. | -| After reliability gate | USB transport only if user tests show local Wi-Fi is a frequent blocker | Compare setup time, stability, and maintenance cost against the existing SRT path before adding a second transport. | -| Later, only with demand | Multiple simultaneous phones, macOS, remote internet transport, recording, effects, or cloud accounts | These expand the protocol, support matrix, or market position. Prioritize from user requests and evidence, not a feature checklist. | - -## One version system for the whole release - -The current release number is copied into `android/gradle.properties`, `obs-plugin/CMakeLists.txt`, both plugin build scripts, the Inno installer, the Windows CI workflow, the website source and metadata, and release prose. The repository test even asserts several literal `1.0.1` strings. The release workflow checks the Android value against the tag, but a local or pull-request build can still produce mismatched artifacts. Android's version code is a separate monotonic install counter, while the `V7`/`V8` OBS names and protocol `1` are compatibility identifiers. These four concepts need distinct names. - -Use one root file, for example `release/version.properties`, as the source for **candidate product version** and **Android version code**: - -```properties -productVersion=1.0.1 -androidVersionCode=1789925364 -``` - -The next public release must use a larger Android version code than the maximum previously published value. Keep `applicationId=dev.openstream.app` and the existing release signing certificate. Never derive the version code from SemVer or reset the current large code to `10200`; Android would reject that as a downgrade. Do not put the signing key in the version file. - -Implement the migration as one focused PR: - -1. Make Gradle read both properties from the root file. Make CMake and the Windows/Linux build scripts read `productVersion` or require the same value as an explicit build argument; remove their `1.0.1` fallbacks. Pass that value into the Inno installer. The website build should receive the version only when showing a candidate; its public download label should come from the **latest published release**, not the current development checkout. -2. Add `tools/check_release_version.py` as a platform-neutral CI gate. It should validate SemVer, a positive Android version code, the release tag, embedded APK metadata, the plugin's built version, the installer version, and the version in each artifact manifest. It should reject a mismatch and check the previous published APK's code and signing certificate. Replace tests that assert literal `1.0.1` with tests for that behavior. -3. Publish one matched release manifest alongside the APK and Windows/Linux packages. Include product version, Android code, protocol version, plugin build identifiers, supported OBS ABI, artifact hashes, and signing-certificate fingerprint. The download page can show the manifest's release version and flag app/plugin mismatches during pairing. -4. Keep wire protocol version, legacy OBS source IDs, and OBS/FFmpeg ABI baseline separate. Increase them only when their own compatibility rules require it. Keep fixed version numbers in historical release notes; do not generate old release history from the current candidate version. - -For example, a future candidate could be `productVersion=1.1.0` with `androidVersionCode=1789925365`, if that code exceeds every published APK for the same application ID. The wire protocol could remain `1`, and an old saved scene could still have its V7 source ID. The release tag would be `v1.1.0`, and CI would prove that the APK, both plugin packages, installer, manifest, and release page all describe `1.1.0` before publication. - -## Trim or consolidate - -- Preserve any legacy source and `shin` protocol IDs that old artifacts actually use, but remove them from new user-facing labels. Prove saved-scene support with old scene fixtures before claiming compatibility. -- Put setup options in OBS source properties and live controls/status in one control area on both supported hosts. Build a Windows dock or choose a cross-platform alternative before removing existing property controls. Keep the Android screen focused on phone selection, preview, camera controls, and a clear Stop action. -- Hide raw host, port, and latency fields behind an Advanced path. Offer manual pairing only when discovery fails or the user chooses it. -- Stop promoting unvalidated claims such as “reliable reconnects” or specific low latency in public copy until device measurements support them. Keep the existing technical target values in protocol docs. -- Keep the Python receiver as a developer tool. It should not become another supported end-user path. -- Avoid effects, built-in recording, and a cloud account in the next release. OBS already owns production and recording; OpenStream's job is to supply a dependable phone camera source. - -## A practical release sequence - -1. **First evidence pass.** Run the current candidate with at least three distinct Android devices and both supported OBS hosts. Measure setup completion, 30-minute stability, reconnect time, audio/video offset, and support questions. Fix any crash, stuck reservation, or silent media failure first. -2. **Lifecycle and pairing pass.** Move session ownership out of the activity, add observable connection states, then version and secure the pairing/control protocol. Test candidate/previous combinations and add scene adapters only for IDs confirmed in old release artifacts. -3. **Professional onboarding pass.** Align branding, simplify the OBS setup and live dock, add actionable diagnostics, and test the entire flow with people who have never built the repo. -4. **Release operations pass.** Publish a license and support policies, automate compatibility gates where possible, record manual acceptance evidence for the exact release artifacts, and release the Android APK and matching OBS plugin together. - -Before claiming better performance, capture before-and-after latency, dropped frames, CPU, memory, battery, and heat on the same devices and network. Before claiming broad support, track the percentage of first-time users who reach live video, time to first frame, and successful reconnect rate. - -For a small public beta, recruit 10 to 20 users who did not build the project, including at least two Android vendors and both OBS host platforms. Give each tester the matched APK and plugin packages, a short task script, and one feedback form. Ask them to install, reach live video, switch a lens, recover from a network drop, and update to the next build. Record where they stop, the error they see, and the time it takes to recover. Review reports weekly, publish known issues with workarounds, and use the measured failures to choose the next change. Make logs opt-in and remove IP addresses and pairing data before upload. - -## Position against other products - -| Product | Strong fit | Trade-off for OpenStream users | -| --- | --- | --- | -| OpenStream | A local Android-to-native-OBS source with camera controls and no account | One phone, LAN-only, limited release/device proof, and a narrower platform matrix. | -| [DroidCam OBS](https://droidcam.app/obs/) | Established OBS phone-camera plugin with Wi-Fi and USB and support for multiple devices | OpenStream cannot win on transport or device breadth yet; it can compete on a focused open, local, diagnosable workflow if licensing and reliability evidence catch up. | -| [Camo Studio](https://camo.com/studio) | Polished camera setup, USB/wireless connection, video adjustments, and broad desktop app use | Its wider creative scope is a poor immediate target for OpenStream. Match the clarity of setup and controls before adding effects. | -| [VDO.Ninja](https://docs.vdo.ninja/getting-started/mobile-phone-camera-into-webcam) | Browser-based phone camera into an OBS Browser Source, useful for remote guests and quick joining | OpenStream's native local source can be simpler for repeated LAN use if pairing and recovery become predictable; remote contribution is a separate product problem. | - -OpenStream's useful position is a dependable, privacy-conscious Android camera for OBS on the same network. The strongest near-term advantage would be predictable reconnection and clear diagnosis, backed by a published device matrix. It does not need to match Camo's editing tools or VDO.Ninja's remote guest workflow. - -## Checks performed for this review - -- `python -m pytest -q`: 115 passed. -- Five previously built native test executables under `obs-plugin/build/`: passed when run directly. This does not prove that those binaries match the current source commit. -- `npm run build` in `website`: passed. -- Android unit tests and lint: attempted, blocked because this workstation has no configured Android SDK. -- `ctest`: unavailable on this workstation; CI defines native CTest jobs. -- No physical Android device or OBS session was used. No runtime latency, heat, stability, or first-use success rate was measured. diff --git a/docs/protocol.md b/docs/protocol.md deleted file mode 100644 index d937a20c..00000000 --- a/docs/protocol.md +++ /dev/null @@ -1,300 +0,0 @@ -# OpenStream Protocol Specification - -## Overview - -OpenStream V1.0.1 currently uses version 1 of its LAN wire protocol. Product -version numbers and protocol versions are independent: `SHIN/1` and -`SHIN_PHONE/1` below identify the protocol, not the app release. - -OpenStream uses three communication channels between the Android phone and OBS: - -1. **Media Stream** - SRT/MPEG-TS for video + audio (phone -> OBS) -2. **Discovery** - UDP multicast/broadcast beacons (bidirectional) -3. **Control** - HTTP POST for remote camera commands (OBS -> phone) - ---- - -## Media Stream - -V1 media transport uses SRT from Android caller to Windows listener. - -### SRT URLs - -Android caller: - -```text -srt://:9100?mode=caller&latency=120 -``` - -OBS listener: - -```text -srt://0.0.0.0:9100?mode=listener&latency=120 -``` - -The supported latency range is `80-200 ms`; both pairing links and the OBS -source clamp values to that range. - -### Container Format - -The Android app muxes encoded video and audio into **MPEG-TS** before sending -over SRT. This ensures FFmpeg/OBS can read the phone stream as a standard -transport stream. - -### Video Payload - -- Codec: `video/avc` by default; `video/hevc` only for an explicit hardware profile -- Source: MediaCodec hardware encoder -- Resolution: 1920x1080 default -- Frame rate: 30 fps default -- Bitrate: 12 Mbps default (bounded to 8-50 Mbps) -- Keyframe interval: 1 second -- No B-frame dependency in target encoder profile - -### Audio Payload - -- Codec: AAC -- Source: MediaCodec audio encoder from device microphone -- Sample rate: 48 kHz -- Channels: Mono -- Output: Separate OBS mixer channel for independent volume control - ---- - -## Discovery Protocol - -### OBS -> Phone (Listener Advertisement) - -When the OBS source listener starts, it broadcasts a UDP beacon every 1 second -on port `51615`: - -**Broadcast destinations:** -- Subnet broadcast addresses (computed from local interfaces) -- Multicast group `239.255.43.99` -- Fallback `255.255.255.255` - -**Beacon format:** - -```text -SHIN/1 {"type":"dev.shin.listener","version":1,"name":"shin","instanceId":"...","sourceInstanceId":"...","slotId":"...","slotLabel":"CAM A","pairingUrl":"shin://connect?...","host":"","listenerPort":9100,"latencyMs":120,"bitrateMbps":12,"busy":false} -``` - -| Field | Type | Description | -|-------|------|-------------| -| `type` | string | Always `dev.shin.listener` | -| `version` | int | Protocol version (currently `1`) | -| `name` | string | OBS source display name | -| `instanceId` | string | Unique source instance identifier | -| `sourceInstanceId` | string | Stable OBS source instance used for reservations | -| `slotId` | string | OBS camera slot identifier | -| `slotLabel` | string | Human-readable camera slot label, such as `CAM A` | -| `pairingUrl` | string | Deep-link URL for QR/manual pairing, including the source control token | -| `host` | string | OBS machine IP address | -| `listenerPort` | int | SRT listener port | -| `latencyMs` | int | Configured SRT latency | -| `bitrateMbps` | int | Expected stream bitrate | -| `busy` | bool | Whether the source is already receiving a stream | - -### Phone -> OBS (Phone Advertisement) - -The Android app advertises itself on the same multicast group: - -**Beacon format:** - -```text -SHIN_PHONE/1 {"type":"dev.shin.phone","version":1,"name":"","instanceId":"...","host":"","listenerPort":9100,"controlPort":9101,"latencyMs":120,"codec":"video/avc","width":1920,"height":1080,"fps":30,"bitrateMbps":12,"busy":false,"reservedBy":""} -``` - -| Field | Type | Description | -|-------|------|-------------| -| `type` | string | Always `dev.shin.phone` | -| `version` | int | Protocol version (currently `1`) | -| `name` | string | Device model name | -| `instanceId` | string | Unique phone advertisement identifier | -| `host` | string | Phone IP address | -| `listenerPort` | int | Phone's SRT port | -| `controlPort` | int | HTTP control server port | -| `latencyMs` | int | Configured SRT latency | -| `width` | int | Stream width in pixels | -| `height` | int | Stream height in pixels | -| `fps` | int | Stream frame rate | -| `bitrateMbps` | int | Stream bitrate | -| `busy` | bool | Whether the phone is already reserved or streaming | -| `reservedBy` | string | OBS source instance that currently owns the reservation, or empty | - -OBS keeps a registry of discovered phones keyed by `instanceId`. Each -The OpenStream source has a `selected_phone_id` setting. `auto` selects the first -non-busy phone; any other value binds that source to one specific phone. -Selection alone does not grant ownership: the source must successfully call -`/reserve` before it may open media or send camera controls. A phone whose -`reservedBy` names a different source is unavailable to that source, even if -its ID is selected manually. On disconnect, the owner calls `/release`; a -failed release is retried and remains visible in the OBS status/log. - -### Fallback Pairing - -If discovery is blocked, the OBS source exposes a deep-link URL: - -```text -shin://connect?slotId=&slotLabel=&sourceInstanceId=&host=&port=&latency=&name=... -``` - -The Android app also accepts the legacy `openstream://connect` scheme so -previously saved pairing links continue to open the app. - -This can be encoded as a QR code or entered manually in the Android app. - ---- - -## Control Protocol - -The Android app runs a lightweight HTTP server on port `9101` for remote camera -control from OBS. Requests, headers, and bodies are size-limited and parsed as -UTF-8 bytes. Protocol V1 does not authenticate this channel, so discovery and -camera control must be used only on a trusted LAN. Authentication must be added -as a versioned, atomic Android-and-OBS protocol upgrade rather than enabled in -only one component. - -### Reserve Phone - -```http -POST /reserve -Content-Type: application/json - -{ - "sourceInstanceId": "openstream-...", - "slotId": "slot-a", - "slotLabel": "CAM A", - "bitrateMbps": 12 -} -``` - -Marks the phone as reserved before OBS opens the SRT stream. The phone rejects -reservations from other source instances while reserved or streaming. -`sourceInstanceId` is required. `slotId`, `slotLabel`, and `bitrateMbps` provide -slot metadata and the requested streaming bitrate. - -### Release Phone - -```http -POST /release -Content-Type: application/json - -{"sourceInstanceId":"openstream-..."} -``` - -Clears the reservation after disconnect. - -The release is idempotent for the owning `sourceInstanceId`; a request from a -different source cannot clear another source's reservation. OBS sends release -when a source is stopped or removed, including a source that was reserved but -did not yet receive SRT media. - -### Endpoints - -#### Set Zoom - -```http -POST /zoom -Content-Type: application/json - -{"value": 2.5} -``` - -Sets the digital zoom level. Value range depends on the active camera lens. - -#### Toggle Torch - -```http -POST /torch -Content-Type: application/json - -{"enabled": true} -``` - -Turns the flashlight on (`true`) or off (`false`). - -#### Switch Lens - -```http -POST /lens -Content-Type: application/json - -{"lens": "1×"} -``` - -Switches to the specified camera lens. Known values: - -| Value | Camera | -|-------|--------| -| `"0.5×"` | Ultra-wide | -| `"1×"` | Wide (default back camera) | -| `"2×"` | Telephoto | -| `"Front"` | Front-facing camera | - -Available lenses depend on the device hardware. - -#### Identify Phone - -```http -POST /identify -Content-Type: application/json - -{"label":"CAM A","subtitle":"Wide shot"} -``` - -Temporarily shows the OBS camera-slot label on the phone. - -#### Read Status - -```http -GET /status -``` - -Returns the current zoom range, selected lens, available lenses, and -reservation owner. - -### Response - -All control endpoints return HTTP `200 OK` on success. Successful `POST` -responses contain `"ok": true` (and may include endpoint-specific fields), for -example: - -```json -{"ok":true} -``` - -## Compatibility and Release Ordering - -Android and the OBS plugin are two halves of one protocol. Adding a required -field, token, endpoint, or validation rule is a compatibility change even if -the `SHIN/1` prefix remains unchanged. Such changes must be tested as an -old/new client matrix and released atomically as one full release containing -both artifacts. - -Keep required protocol changes in one release containing both the Android APK -and OBS plugin. Additive optional fields may be rolled out independently only -when both older components demonstrably ignore them safely. - ---- - -## Telemetry - -Telemetry is separate from media. The Android app samples: - -```json -{ - "deviceName": "Google Pixel", - "codec": "video/avc", - "width": 1920, - "height": 1080, - "fps": 30, - "bitrate": 12000000, - "batteryPercent": 87, - "wifiRssi": -48, - "temperatureCelsius": null, - "encoderState": "streaming" -} -``` - -V1 telemetry is used internally by the app. diff --git a/docs/release-notes-template.md b/docs/release-notes-template.md deleted file mode 100644 index a8cb663d..00000000 --- a/docs/release-notes-template.md +++ /dev/null @@ -1,44 +0,0 @@ -# OpenStream V1.0.1 - -OpenStream V1.0.1 is a fixed Android-to-OBS camera release with V8 source naming, V7 scene compatibility, universal Android packaging, and install downloads for both surfaces. - -## What's New - -| Area | Change | -|---|---| -| OBS setup | Source properties now follow `1. Camera Slot`, `2. Live Camera Controls`, and `3. Network & Pairing (Advanced)`. | -| Scene compatibility | Existing `OpenStream V7` scene sources keep loading through a compatibility source id. | -| Pairing copy | OBS now explains the phone-side slot selection flow instead of leading with SRT details. | -| Setup docs | The walkthrough now includes stale-plugin checks for Program Files, ProgramData, and per-user OBS plugin folders. | -| OBS compatibility | Windows plugin releases target OBS Studio 32.2.1 x64 and its FFmpeg 62/62/60/9 ABI. | - -## Downloads - -| File | Use | -|---|---| -| `openstream-android.apk` | Install this on your Android phone. | -| `openstream-android.apk.sha256` | Verify the downloaded APK before installation. | -| `openstream-obs-plugin-installer-windows-x64.exe` | Recommended Windows installer for the OBS plugin. | -| `openstream-obs-windows-x64.zip` | Manual plugin package with DLL and install scripts. | -| `openstream-obs-windows-x64.zip.sha256` | Verify the downloaded manual plugin package. | - -> [!NOTE] -> The Android APK is release-signed and accompanied by a SHA-256 checksum. Public releases fail instead of publishing a debug-signed fallback when signing inputs are unavailable. - -## Install - -1. Install the APK on your Android phone. -2. Run the OBS plugin installer on your Windows OBS PC. -3. Restart OBS Studio. -4. Add an `OpenStream V8` source. -5. Open the Android app on the same Wi-Fi network and tap the discovered OBS slot. - -For the screenshot walkthrough, read [`docs/set-up.md`](https://github.com/YashasVM/OpenStream/blob/main/docs/set-up.md). - -## Notes - -- Windows OBS plugin only. -- Existing source names may still say `OpenStream V7`; rename them in OBS if you want the scene label to match V1.0.1. -- Use 5 GHz or Wi-Fi 6 for best results. -- Keep both devices on the same subnet. -- Please report issues on GitHub. diff --git a/docs/release.md b/docs/release.md deleted file mode 100644 index a8001025..00000000 --- a/docs/release.md +++ /dev/null @@ -1,164 +0,0 @@ -# Release Guide - -OpenStream releases should give users direct installable assets instead of making them use GitHub source-code archives. - -The Windows plugin baseline is OBS Studio **32.2.1 x64**. Its package contains only -`openstream-obs.dll`; it deliberately does not ship a second copy of OBS/FFmpeg -DLLs. Linux packages are built on Ubuntu 24.04 and use the host's OBS, FFmpeg, -and Qt libraries. Install them only on compatible x86_64 distributions; the -archive does not bundle those system libraries. - -## Release Assets - -| Asset | Audience | Purpose | -|---|---|---| -| `openstream-android.apk` | Android users | Signed install package for the OpenStream camera app. | -| `openstream-android.apk.sha256` | Android users and automation | SHA-256 checksum for the exact APK in the release. | -| `openstream-obs-plugin-installer-windows-x64.exe` | Windows OBS users | Recommended one-click OBS plugin installer. | -| `openstream-obs-plugin-installer-windows-x64.exe.sha256` | Windows users and automation | SHA-256 checksum for the installer. | -| `openstream-obs-windows-x64.zip` | Technical users | Manual plugin package with DLL and install scripts. | -| `openstream-obs-windows-x64.zip.sha256` | Technical users and automation | SHA-256 checksum for the exact manual plugin package. | -| `openstream-obs-linux-x86_64.tar.gz` | Native Linux OBS users | Linux plugin module and install script for compatible distro OBS builds. | -| `openstream-obs-linux-x86_64.tar.gz.sha256` | Linux users and automation | SHA-256 checksum for the exact Linux package. | - -### Android compatibility and updates - -The release APK keeps the published application ID `dev.openstream.app` and -release signing identity so existing installs can update in place. -Android can update an existing installation only when the application ID and -signing certificate match and the new APK has a higher version code. The release -workflow checks the version code against prior tagged releases and compares the -APK certificate with the latest published APK. Keep the four Android signing -secrets below stable; replacing the keystore or alias prevents in-place updates -and requires users to uninstall first, losing app data. - -The APK is universal with `minSdk 29` and native libraries for `arm64-v8a`, -`armeabi-v7a`, `x86`, and `x86_64`. Camera lens availability and device-specific -Camera2 behavior still require checking on target phones. - -## Automated Release - -Set both `openstream.versionName` and `openstream.versionCode` in -`android/gradle.properties`, then create and push a matching `vMAJOR.MINOR.PATCH` -tag that points to that commit. The version name must equal the tag without its -leading `v`; the version code must be a positive integer greater than every -previous release code and no greater than Android's 2,100,000,000 limit. - -```powershell -git tag v1.0.1 -git push origin v1.0.1 -``` - -The same workflow can be run manually with an already pushed release tag. It -builds the commit referenced by that tag and rejects malformed or mismatched -tags. It does not create a release from the current default branch. Older -releases used the tag commit timestamp as the Android version code; the -workflow uses that value when checking historical tags without the canonical -`openstream.versionCode` property. - -The `Release` workflow builds: - -| Job | Output | -|---|---| -| Android APK | `openstream-android.apk`, `openstream-android.apk.sha256` | -| OBS plugin package | `openstream-obs-windows-x64.zip`, `openstream-obs-windows-x64.zip.sha256` | -| OBS plugin installer | `openstream-obs-plugin-installer-windows-x64.exe`, `openstream-obs-plugin-installer-windows-x64.exe.sha256` | -| Linux OBS plugin | `openstream-obs-linux-x86_64.tar.gz`, `openstream-obs-linux-x86_64.tar.gz.sha256` | - -The publish job runs only after repository tests, Android unit tests and lint, -the signed Android build, and both Windows and Linux plugin builds succeed. It -downloads all artifacts, verifies each published binary's SHA-256 sidecar, and -then runs `gh release create`. - -Public releases require all Android signing secrets. Missing or incomplete -signing inputs fail the workflow; it never publishes a debug-signed fallback. -Debug APKs remain available only as pull-request and local development -artifacts. - -### Android and OBS compatibility gate - -Changes to discovery, pairing, required control tokens, reservation behavior, -or media framing must ship atomically in a full release containing both the -Android APK and OBS artifacts. - -For example, an Android build that introduces a required control token cannot pair with an -older OBS plugin whose beacon does not contain that token. Tests and review -must cover the old/new compatibility matrix before changing either side of the -wire contract. - -### Android Signing Secrets - -Configure all of these GitHub Actions secrets before publishing a full release: - -| Secret | Purpose | -|---|---| -| `OPENSTREAM_RELEASE_KEYSTORE_BASE64` | Base64-encoded Android keystore file. | -| `OPENSTREAM_RELEASE_STORE_PASSWORD` | Keystore password. | -| `OPENSTREAM_RELEASE_KEY_ALIAS` | Release key alias. | -| `OPENSTREAM_RELEASE_KEY_PASSWORD` | Release key password. | - -## Local Plugin Packaging - -Build and package the OBS plugin without installing it locally: - -```powershell -$env:OPENSTREAM_SKIP_INSTALL = "1" -$env:OPENSTREAM_PLUGIN_PACKAGE_DIR = "$PWD\artifacts" -.\build_plugin.bat -``` - -Output: - -```text -artifacts/openstream-obs-windows-x64.zip -``` - -The zip contains: - -```text -openstream-obs.dll -Install-OpenStreamPlugin.ps1 -install-openstream-plugin.bat -``` - -To use an OBS install outside `C:\Program Files\obs-studio`, set: - -```powershell -$env:OPENSTREAM_OBS_INSTALL = "D:\Apps\obs-studio" -``` - -## Local Android Build - -Android release validation should use the real streaming build. Signed release validation requires a keystore: - -```powershell -cd android -$env:OPENSTREAM_RELEASE_KEYSTORE = "$PWD\openstream-release.keystore" -$env:OPENSTREAM_RELEASE_STORE_PASSWORD = "" -$env:OPENSTREAM_RELEASE_KEY_ALIAS = "" -$env:OPENSTREAM_RELEASE_KEY_PASSWORD = "" -.\gradlew.bat :app:assembleRelease -``` - -Do not pass `-Popenstream.nonStreamingCiBuild=true` for release artifacts. If -signing secrets are unavailable, use `:app:assembleDebug` for local validation -only; do not publish that APK as a release. - -## Release Checklist - -- Confirm the README links point to the release tag being published. -- Confirm the setup guide links to the same APK, installer EXE, and plugin zip. -- Confirm pytest, Android unit tests, lint, and Android, Windows, and Linux release builds passed. -- Confirm `openstream-android.apk.sha256` matches the APK. -- Confirm `openstream-obs-plugin-installer-windows-x64.exe.sha256` matches the installer. -- Confirm `openstream-obs-windows-x64.zip.sha256` matches the manual plugin package. -- Confirm `openstream-obs-linux-x86_64.tar.gz.sha256` matches the Linux package. -- Confirm OBS lists `OpenStream V8` and can still load saved `openstream_phone_v7_source` scenes. -- Confirm the dependency report names `avformat-62.dll`, `avcodec-62.dll`, `avutil-60.dll`, and `swscale-9.dll`, and the clean OBS 32.2.1 log has no OpenStream module-load error. -- Seed `openstream-obs.dll`, run both installer forms, and confirm stale Program Files, ProgramData, and AppData copies were migrated without touching OBS settings or scenes. -- Confirm the Android APK is release-signed and installable over the previous public release. -- Confirm the signing identity matches the previous release and the version code increased. -- Confirm protocol-affecting Android and OBS changes are released together and pass the old/new compatibility matrix. -- Confirm the GitHub release assets are attached, not only source-code archives. -- Confirm the repository website is set to `https://openstream.pages.dev`. -- Confirm the release notes link users to [`docs/set-up.md`](set-up.md). diff --git a/docs/reliability-audit.md b/docs/reliability-audit.md deleted file mode 100644 index a192df32..00000000 --- a/docs/reliability-audit.md +++ /dev/null @@ -1,31 +0,0 @@ -# Solo-camera reliability audit - -This is a reviewed first hardening pass, not production certification. Muse Spark 1.3 through opencode performed the audit and Linux implementation. Its Android lifecycle rewrite failed review and a new behavioural test; that rewrite and its test were removed together. Existing Android media behaviour is retained. - -## Confirmed defects fixed - -- Linux control socket writes could raise SIGPIPE on disconnect. Per-send suppression now fails the command without terminating OBS; a forked test reproduces the failure and verifies the fix. -- Linux CMake could not find distro OBS headers, required Windows-style configuration and had no install/CI path. Native Linux now builds, packages and installs against system dependencies. -- The OBS JSON scanner could read digits from another field, misread negatives and mishandle escaped names. Typed Qt JSON parsing rejects malformed values; production parser tests cover these cases. -- Android manual connect used SRT caller mode, but OBS always called the phone. An explicit advanced manual-receive mode now opens an OBS listener. The production settings adapter is tested. -- Refresh Phones did not repopulate its properties list. It now refreshes from current discovery data while retaining an unavailable saved selection. -- Multi-camera creation was still exposed. A process-wide ownership gate permits one active source; conflicting sources get an explanation. Concurrent acquisition and production start rejection are tested. Reuse Add Existing across scenes. Legacy scene/protocol identities still load. - -## Checks - -- Existing Python repository checks: 92 passed. -- Linux CMake build: passed with libobs 32.2.2, avformat 63, Qt 6.11.2. -- Five native CTest suites passed: lifecycle/control contracts, SIGPIPE, concurrent solo ownership, typed JSON, production source settings. -- Linux tarball extraction and isolated/repeated installer execution passed. -- Website production build passed. -- Android unit tests, lint and native APK build: passed locally with the real bundled SRT libraries (all four Android ABIs). -- Windows build is configured in existing CI; not executed locally. - -## Release blockers / remaining work - -- Android still owns sessions in MainActivity, can block during lifecycle shutdown, and stops streaming when backgrounded. Moving this into a tested session owner remains required; the rejected rewrite is not shipped. -- No physical Android-to-OBS acceptance run was performed. Verify 30-minute streaming, screen/display behaviour, camera changes, microphone failures, A/V sync, packet loss and reconnects on both Windows and Linux before release. -- OBS hardware decode is not wired: current receiver explicitly warns and uses software decoding. Phone encoding remains hardware based. -- Native distro Linux only; Flatpak/Snap and IPv6-only networks are not validated. No new release was published. -- Vendor-specific codec config handling and timestamp recovery need device evidence. Audit hypotheses were not treated as confirmed defects or patched speculatively. -- This audit is not an exhaustive proof that no other bugs exist. No performance improvement is claimed or benchmarked. diff --git a/docs/set-up.md b/docs/set-up.md deleted file mode 100644 index a2bc9e12..00000000 --- a/docs/set-up.md +++ /dev/null @@ -1,206 +0,0 @@ -# OpenStream Set-Up Guide - -This guide is the slower, screenshot-led path for installing OpenStream V1.0.1 on an Android phone and a Windows OBS Studio PC. - -For native Linux, follow [Linux setup](linux.md). The same phone pairing and -per-source ownership rules apply on Linux and Windows. - -For the fast technical version, use the [README quick start](../README.md#quick-start). - ---- - -## What You Need - -| Requirement | Details | -|---|---| -| Android phone | Android 10 or newer is recommended. The phone must support Camera2. | -| Windows PC | OBS Studio installed on Windows x64. | -| Same network | Phone and PC must be on the same Wi-Fi or LAN subnet. | -| Release files | V1.0.1 APK for the phone, installer EXE for the OBS plugin. | - -> [!TIP] -> If discovery does not work, temporarily disable VPNs, guest Wi-Fi, and router client isolation. - ---- - -## 1. Download the Release Files - -Open the latest OpenStream V1.0.1 release and download: - -| File | Install on | Use | -|---|---|---| -| [`openstream-android.apk`](https://github.com/YashasVM/OpenStream/releases/latest/download/openstream-android.apk) | Android phone | Installs the camera app. | -| [`openstream-obs-plugin-installer-windows-x64.exe`](https://github.com/YashasVM/OpenStream/releases/latest/download/openstream-obs-plugin-installer-windows-x64.exe) | Windows PC | Installs the OBS plugin automatically. | -| [`openstream-obs-windows-x64.zip`](https://github.com/YashasVM/OpenStream/releases/latest/download/openstream-obs-windows-x64.zip) | Windows PC | Manual fallback package. | - -![Release downloads screenshot](assets/setup/release-downloads.svg) - -> [!NOTE] -> The GitHub page also shows `Source code` downloads. Those are for developers. Most users only need the APK and installer EXE. - ---- - -## 2. Install the Android App - -1. Move `openstream-android.apk` to your Android phone. -2. Open the APK from your Downloads app or file manager. -3. If Android blocks the install, allow installs from that app when prompted. -4. Open `OpenStream`. -5. Allow camera and microphone permissions. - -The app should open directly into a camera preview. - ---- - -## 3. Install the OBS Plugin on Windows - -1. Close OBS Studio. -2. Run `openstream-obs-plugin-installer-windows-x64.exe`. -3. Accept the Windows administrator prompt. -4. Keep the OBS folder as `C:\Program Files\obs-studio` unless you installed OBS somewhere else. -5. Finish the installer. -6. Reopen OBS Studio. - -![Windows installer screenshot](assets/setup/windows-installer.svg) - -The installer copies `openstream-obs.dll` into: - -```text -C:\Program Files\obs-studio\obs-plugins\64bit\ -``` - -> [!IMPORTANT] -> Close OBS before installing or replacing the plugin. Windows can keep the old DLL loaded while OBS is open. - -### Upgrading from an Older OpenStream Plugin - -OpenStream V1.0.1 appears in OBS as `OpenStream V8`. Existing scene sources created with `OpenStream V7` are still supported, but the saved source names may remain `OpenStream V7` until you rename them in OBS. - -If OBS still shows an older OpenStream source after installing V1.0.1, check these plugin locations and remove or replace stale copies: - -```text -C:\Program Files\obs-studio\obs-plugins\64bit\openstream-obs.dll -C:\ProgramData\obs-studio\plugins\openstream-obs\bin\64bit\openstream-obs.dll -%APPDATA%\obs-studio\plugins\openstream-obs\bin\64bit\openstream-obs.dll -``` - -### Manual Plugin Install - -Use this only if the installer EXE is blocked or you want to inspect the files first. - -1. Download `openstream-obs-windows-x64.zip`. -2. Extract the zip. -3. Right-click `install-openstream-plugin.bat`. -4. Choose `Run as administrator`. -5. Restart OBS Studio. - -You can also copy the DLL yourself: - -```text -openstream-obs.dll -> C:\Program Files\obs-studio\obs-plugins\64bit\openstream-obs.dll -``` - ---- - -## 4. Add OpenStream in OBS - -1. Open OBS Studio. -2. In `Sources`, click `+`. -3. Choose `OpenStream V8`. -4. In `1. Camera`, keep automatic phone pairing selected for the first test. -5. Keep one OpenStream source. Use **Add Existing** to reuse it in other scenes; remove extra legacy camera sources. -6. Use `Refresh Phones` if your Android device appears after the properties window is already open. -7. Leave `3. Network & Pairing (Advanced)` collapsed unless discovery fails. -8. Click OK. - -![OBS source screenshot](assets/setup/obs-source.svg) - -The source can stay blank until a phone connects. That is normal. - ---- - -## 5. Connect the Phone - -1. Put the phone and PC on the same Wi-Fi network. -2. Open OpenStream on the phone. -3. Wait for the OBS device to appear. -4. Tap your OBS computer in the discovered devices list. -5. The phone camera should appear in OBS. - -![Android connect screenshot](assets/setup/android-connect.svg) - -If the phone does not find OBS, open the source’s **Network & Pairing (Advanced)** settings, enable **Receive manual connection from phone**, and apply. Then use manual connect on the phone. This uses the phone’s camera controls; remote controls in OBS require automatic pairing. Both devices still need direct LAN connectivity: - -| Value | Default | -|---|---| -| OBS PC IP address | Your PC's LAN IP, for example `192.168.1.25` | -| SRT port | `9100` | -| Latency | `120 ms` | - -The phone uses its own control endpoint on port `9101`; do not enter that port -as the media port. Manual receive mode is an advanced fallback: it makes the -phone call the OBS listener directly, while automatic pairing reserves the -selected phone before opening its SRT stream. - -### Connect and disconnect a source - -Each OBS source is one camera slot. In the source properties, choose a phone -under **1. Camera**, then click **Connect / Reconnect**. With **Automatic -pairing**, OBS reserves that phone and the phone shows the selected slot label. -The source status changes from `Waiting` to `Reserved` and then `Live` after -SRT media arrives. Use **Add Existing** when the same camera should appear in -another scene; creating another active source for the same camera is blocked. - -To disconnect, stop or remove the source, or use the source's **Disconnect** -control when it is shown. OBS releases the reservation and the phone returns -to its available state. Disconnecting one source does not stop another phone. -If the phone is reserved but media has not arrived, disconnect still releases -the reservation; a short reconnect window may keep the same phone preferred -when the network briefly drops. - -On Android, tap an OBS device in the discovered list to connect. The active -card shows the slot label and a **Disconnect** action. Tap it before selecting -another OBS slot. The phone's **Settings** screen also provides **Save** and -**Save and connect** for manual pairing; the saved host, port, latency and -listening port survive a disconnect and app restart. - ---- - -## 6. Confirm Audio and Controls - -In OBS: - -1. Look for the OpenStream audio channel in the mixer. -2. Open the OpenStream source properties. -3. Use `2. Live Camera Controls`. -4. Try the zoom slider. -5. Try torch on/off if the selected phone camera supports it. -6. Switch rear/front camera from the source properties or phone UI. -7. Use `Identify Phone` to show a connection label on your phone. - ---- - -## Troubleshooting - -| Problem | Try | -|---|---| -| Installer cannot find OBS | Re-run it and choose the folder that contains `bin\64bit\obs64.exe`. | -| Windows blocks the EXE | Use `More info` then `Run anyway`, or use the manual zip install. | -| OpenStream V8 is missing in OBS | Confirm `openstream-obs.dll` is in one of the plugin folders above, remove stale older copies, then restart OBS. | -| Old OpenStream source is still visible | OBS may be loading an older all-users DLL from `C:\ProgramData\obs-studio\plugins\openstream-obs\bin\64bit\`. Replace it with the V1.0.1 DLL or remove it. | -| Phone cannot see OBS | Put both devices on the same Wi-Fi, disable VPNs, and check guest/client isolation. | -| Camera stays blank | Start with media port `9100`, latency `120 ms`, and one phone only. | -| Audio is missing | Grant microphone permission on Android and check the OBS mixer channel. | -| Stream stutters | Use 5 GHz or Wi-Fi 6, move closer to the router, and avoid congested networks. | - ---- - -## Developer Notes - -The Python receiver is a developer/debug path only; it is not part of the normal user workflow. - -```powershell -python tools/openstream_receiver.py --port 9100 --latency-ms 120 --ffplay -``` - -Use it only when debugging SRT transport outside OBS. diff --git a/docs/testing.md b/docs/testing.md deleted file mode 100644 index 7415a424..00000000 --- a/docs/testing.md +++ /dev/null @@ -1,104 +0,0 @@ -# OpenStream Test Plan - -## Automated merge and publish gates - -Every pull request and push to `main` must pass: - -- `python -m pytest -q` -- Android debug unit tests -- Android lint -- The native-SRT Android debug build -- The Windows OBS plugin and installer build - -Full releases also require a signed Android release build and matching -Android/OBS artifacts before the release is created. - -The Python repository-contract tests are fast guardrails, not substitutes for -executing Kotlin or C++ production code. New parsers and protocol state -machines should receive native unit tests rather than Python copies of their -implementation. - -## Device acceptance tests - -- One Android phone streams 1080p30 for 30 minutes without receiver crash. -- Android discovers a OpenStream OBS source without manual IP entry. -- Tapping a discovered OBS device starts the stream directly. -- Stopping the OBS listener removes the device from Android discovery within about 5 seconds. -- SRT reconnect completes within 2 seconds after a short Wi-Fi interruption. -- OBS receives video through the selected `shin` source. -- OBS receives mono AAC audio at 48 kHz on the source's mixer channel. -- The OBS source shows only the phone camera feed, never the Android screen. -- Starting OBS Virtual Camera while the source is live does not create an - ever-growing delay; video remains within the configured low-latency window. -- A clap or tone remains aligned between the OBS video and mono audio paths - after Virtual Camera starts and after a reconnect. -- Telemetry updates at least once per second. - -## Android/OBS compatibility matrix - -Before changing discovery fields, pairing URLs, authentication, reservations, -or media framing, test all four combinations: - -| Android | OBS plugin | Expected result | -|---|---|---| -| Previous | Previous | Existing workflow remains operational | -| Previous | Candidate | Candidate remains backward-compatible or provides a clear upgrade path | -| Candidate | Previous | Candidate remains backward-compatible | -| Candidate | Candidate | New behavior works end to end | - -A candidate Android build that introduces a required control token must not be published -while the generally available OBS plugin still advertises tokenless beacons. -Protocol-breaking changes ship as one full release with both artifacts. - -## Network impairment tests - -Use network tooling or router controls to test: - -- 1% packet loss -- 3% packet loss -- 5% packet loss -- 50 ms jitter -- temporary disconnect under 5 seconds - -Expected behavior: - -- The stream may degrade, but the app should not crash. -- Reconnect attempts should continue automatically. -- Telemetry should report degraded state. -- SRT latency can be increased from 120 ms to 200 ms for lossy networks. - -## Developer receiver - -`tools/openstream_receiver.py` remains available for FFmpeg/SRT smoke tests -without OBS. It is not part of the normal user workflow. - -## Thermal tests - -Run 1080p30 for 30 minutes and log: - -- Battery level -- Temperature -- Encoder state -- Frame drops -- Bitrate - -If temperature exceeds the warning threshold, the app should recommend lowering bitrate or switching to 720p. - -## Performance measurement record - -The pre-fix defaults and the candidate defaults are measured from the checked-in -profiles and bounded buffers below. Runtime temperature and end-to-end latency -still require a physical phone/OBS run before release sign-off. - -| Item | Before | Candidate | Change | -|---|---:|---:|---:| -| Default video rate | 60 fps | 30 fps | 50% fewer encoded frames | -| Default video bitrate | 50 Mbps | 12 Mbps | 76% lower target bitrate | -| Audio capture buffer | 250 ms | 80 ms | 68% less capture backlog | -| App send path | Inline blocking callback | 768 KiB hard cap | Drops the session instead of growing latency | -| OBS stale-frame threshold | Arrival-time timestamps | 250 ms source-clock backlog | Old frames are dropped and logged | - -For the device run, record the 30-minute temperature delta, average measured -end-to-end latency, maximum audio/video offset, reconnect time, and dropped -frame count for the old and candidate builds under the same Wi-Fi and OBS -Virtual Camera workload. diff --git a/obs-plugin/CMakeLists.txt b/obs-plugin/CMakeLists.txt index 58b33578..63594e33 100644 --- a/obs-plugin/CMakeLists.txt +++ b/obs-plugin/CMakeLists.txt @@ -1,10 +1,9 @@ cmake_minimum_required(VERSION 3.24) -project(openstream_obs_plugin VERSION 1.0.1 LANGUAGES CXX) +project(openstream_obs_plugin VERSION 1.0.2 LANGUAGES CXX) include(CTest) find_package(Threads REQUIRED) -# Version-consistency: keep 1.0.1 in sync with android/app/build.gradle.kts -# ("1.0.1"), tools/installer/openstream-obs-plugin.iss (#define OpenStreamVersion), -# build_plugin.bat OPENSTREAM_VERSION, and build_plugin_linux.sh OPENSTREAM_VERSION. +# Match the Android release version in android/gradle.properties and the +# installer defaults. Release workflows override OPENSTREAM_VERSION from the tag. # Releases ship the APK + plugin atomically. set(OPENSTREAM_VERSION "${PROJECT_VERSION}" CACHE STRING "Version embedded in the OBS plugin") @@ -12,7 +11,7 @@ set(OPENSTREAM_VERSION "${PROJECT_VERSION}" CACHE STRING "Version embedded in th # avformat 62, avcodec 62, avutil 60 and swscale 9. Keeping this declaration # here makes accidental ABI changes fail at configure time instead of at OBS # module-load time. The enforcement below is Windows-only: Linux links the -# distro OBS/FFmpeg system ABI (see the Linux branch and docs/linux.md). +# distro OBS/FFmpeg system ABI. set(OPENSTREAM_OBS_BASELINE "32.2.1") set(OPENSTREAM_FFMPEG_ABI "avformat-62;avcodec-62;avutil-60;swscale-9") @@ -125,7 +124,7 @@ else() # include/library paths are the fallback. There is intentionally no # FFmpeg ABI pin on this path: the module links whatever # libavformat/libavcodec the system OBS was built against (documented in - # docs/linux.md). The Windows release pin above is untouched. + # The Windows release pin above is untouched. find_package(PkgConfig QUIET) if (PkgConfig_FOUND) pkg_check_modules(PC_LIBOBS QUIET libobs) @@ -256,5 +255,5 @@ endif() message(STATUS "Building OpenStream OBS source with FFmpeg SRT input support.") message(STATUS "OpenStream OBS baseline: ${OPENSTREAM_OBS_BASELINE}; FFmpeg ABI: ${OPENSTREAM_FFMPEG_ABI}") if (NOT WIN32) - message(STATUS "OpenStream Linux build: linking system OBS/FFmpeg ABI (no Windows version pin). See docs/linux.md.") + message(STATUS "OpenStream Linux build: linking system OBS/FFmpeg ABI (no Windows version pin).") endif() diff --git a/obs-plugin/src/openstream-dock.cpp b/obs-plugin/src/openstream-dock.cpp index 68f51f1a..98a9a9be 100644 --- a/obs-plugin/src/openstream-dock.cpp +++ b/obs-plugin/src/openstream-dock.cpp @@ -5,6 +5,8 @@ #include #include #include +#include +#include #include #include #include @@ -25,30 +27,34 @@ class OpenStreamDock final : public QWidget { setObjectName("OpenStreamCameraControl"); auto *layout = new QVBoxLayout(this); + layout->setContentsMargins(8, 8, 8, 8); + layout->setSpacing(6); + auto *connection_group = new QGroupBox("Connection", this); + auto *connection_form = new QFormLayout(connection_group); + connection_form->setContentsMargins(8, 8, 8, 8); + connection_form->setVerticalSpacing(5); source_selector_ = new QComboBox(this); source_selector_->setObjectName("openstreamDeviceSelector"); source_selector_->setAccessibleName("OpenStream source"); - layout->addWidget(new QLabel("Source", this)); - layout->addWidget(source_selector_); + connection_form->addRow("Source", source_selector_); phone_selector_ = new QComboBox(this); phone_selector_->setObjectName("openstreamPhoneSelector"); phone_selector_->setAccessibleName("OpenStream phone"); - layout->addWidget(new QLabel("Phone", this)); - layout->addWidget(phone_selector_); + connection_form->addRow("Phone", phone_selector_); status_ = new QLabel("No OpenStream source", this); status_->setObjectName("openstreamStatus"); - layout->addWidget(status_); + connection_form->addRow("Status", status_); auto *name_row = new QHBoxLayout(); - name_row->addWidget(new QLabel("Name", this)); + name_row->addWidget(new QLabel("Name", connection_group)); name_ = new QLineEdit(this); name_->setObjectName("openstreamSourceName"); name_->setAccessibleName("OpenStream source name"); name_->setMaxLength(128); name_->setPlaceholderText("Name this source"); name_row->addWidget(name_); - layout->addLayout(name_row); + connection_form->addRow(name_row); connect(name_, &QLineEdit::editingFinished, this, [this] { renameSelectedSource(); }); @@ -59,7 +65,13 @@ class OpenStreamDock final : public QWidget { disconnect_->setObjectName("openstreamDisconnect"); connection->addWidget(test_connect_); connection->addWidget(disconnect_); - layout->addLayout(connection); + connection_form->addRow(connection); + layout->addWidget(connection_group); + + auto *controls_group = new QGroupBox("Camera controls", this); + auto *controls_layout = new QVBoxLayout(controls_group); + controls_layout->setContentsMargins(8, 8, 8, 8); + controls_layout->setSpacing(5); // These calls only publish into the source's bounded, coalescing slots. // Network work and worker joins stay outside the OBS UI thread. @@ -94,10 +106,10 @@ class OpenStreamDock final : public QWidget { zoom_->setSuffix("x"); apply_zoom_ = new QPushButton("Apply zoom", this); apply_zoom_->setObjectName("openstreamApplyZoom"); - zoom_row->addWidget(new QLabel("Zoom", this)); + zoom_row->addWidget(new QLabel("Zoom", controls_group)); zoom_row->addWidget(zoom_); zoom_row->addWidget(apply_zoom_); - layout->addLayout(zoom_row); + controls_layout->addLayout(zoom_row); connect(apply_zoom_, &QPushButton::clicked, this, [this] { const QByteArray body = QByteArray("{\"value\":") + QByteArray::number(zoom_->value(), 'f', 1) + "}"; @@ -112,10 +124,10 @@ class OpenStreamDock final : public QWidget { lens_rear_->setObjectName("openstreamLensRear"); lens_front_ = new QPushButton("Front", this); lens_front_->setObjectName("openstreamLensFront"); - lens_row->addWidget(new QLabel("Lens", this)); + lens_row->addWidget(new QLabel("Lens", controls_group)); lens_row->addWidget(lens_rear_); lens_row->addWidget(lens_front_); - layout->addLayout(lens_row); + controls_layout->addLayout(lens_row); connect(lens_rear_, &QPushButton::clicked, this, [this] { send("/lens", R"({"lens":"1×"})"); }); @@ -133,7 +145,7 @@ class OpenStreamDock final : public QWidget { torch_row->addWidget(torch_on_); torch_row->addWidget(torch_off_); torch_row->addWidget(identify_); - layout->addLayout(torch_row); + controls_layout->addLayout(torch_row); connect(torch_on_, &QPushButton::clicked, this, [this] { send("/torch", R"({"enabled":true})"); }); @@ -144,6 +156,8 @@ class OpenStreamDock final : public QWidget { send("/identify", R"({"label":"OBS"})"); }); + layout->addWidget(controls_group); + layout->addStretch(); connect(source_selector_, &QComboBox::currentIndexChanged, this, diff --git a/tests/test_prod_hardening_contract.py b/tests/test_prod_hardening_contract.py index b8c4ebdc..fc5b0879 100644 --- a/tests/test_prod_hardening_contract.py +++ b/tests/test_prod_hardening_contract.py @@ -102,12 +102,3 @@ def test_dock_keeps_full_peer_bound_controls(): for required in ('send("/zoom"', 'send("/lens"', 'send("/torch"', 'send("/identify"'): assert required in dock assert dock.count("new QPushButton") == 8 - - -def test_docs_use_canonical_ports(): - arch = read("docs/architecture.md") - assert "51615" in arch - assert "239.255.43.99" in arch - assert ":9100" in arch - assert ":9101" in arch - assert "51515" not in arch diff --git a/tests/test_production_contracts.py b/tests/test_production_contracts.py index 1bc9f343..c75212c6 100644 --- a/tests/test_production_contracts.py +++ b/tests/test_production_contracts.py @@ -134,19 +134,15 @@ def test_pairing_url_acceptance_contract_clamps_network_values() -> None: def test_android_and_obs_release_artifacts_stay_atomic() -> None: android_workflow = read(".github/workflows/android.yml") release_workflow = read(".github/workflows/release.yml") - release_docs = read("docs/release.md") assert "gh release create" not in android_workflow assert "python -m pytest -q" in android_workflow assert ":app:lintDebug" in android_workflow assert "openstream-android.apk.sha256" in android_workflow - assert "android/gradle.properties" in release_docs assert "openstream.versionName" in release_workflow assert "openstream-android-update.json" not in android_workflow assert "openstream-android-update.json" not in release_workflow assert not (ROOT / "android/app/src/main/java/dev/openstream/app/update/AppUpdater.kt").exists() - assert "/releases/latest/download/" in read("README.md") - assert "Android APK and OBS artifacts" in release_docs def test_android_update_surface_is_removed() -> None: @@ -164,10 +160,11 @@ def test_android_update_surface_is_removed() -> None: def test_android_pr_builds_do_not_receive_signing_secrets_or_write_token() -> None: android_workflow = read(".github/workflows/android.yml") - build_job_text = android_workflow.split(" build:", 1)[1] + build_job_text = android_workflow.split(" build:", 1)[1].split(" signed-candidate:", 1)[0] assert "permissions:\n contents: read" in android_workflow assert "OPENSTREAM_RELEASE_KEYSTORE_BASE64" not in build_job_text + assert "if: github.event_name == 'workflow_dispatch'" in android_workflow assert "OPENSTREAM_RELEASE_STORE_PASSWORD" not in build_job_text assert "contents: write" not in build_job_text assert "persist-credentials: false" in build_job_text diff --git a/tests/test_repo_contract.py b/tests/test_repo_contract.py index 18179163..47c0d107 100644 --- a/tests/test_repo_contract.py +++ b/tests/test_repo_contract.py @@ -3,16 +3,6 @@ from _helpers import read_text as read -def test_architecture_documents_practical_v1_transport() -> None: - architecture = read("docs/architecture.md") - assert "MediaCodec hardware AVC/H.264 video encode" in architecture - assert "MediaCodec AAC audio encode" in architecture - assert "SRT caller" in architecture - assert "UDP discovery" in architecture - assert "PTP" in architecture - assert "## Research Track" not in architecture - - def test_android_project_declares_camera_media_codec_srt_discovery_boundaries() -> None: app = read("android/app/src/main/java/dev/openstream/app/MainActivity.kt") discovery = read("android/app/src/main/java/dev/openstream/app/discovery/PhoneDiscoveryAdvertiser.kt") @@ -252,32 +242,10 @@ def test_receiver_validates_srt_support() -> None: assert "mode=caller" in receiver -def test_protocol_documents_media_and_telemetry_contracts() -> None: - protocol = read("docs/protocol.md") - assert "MediaCodec" in protocol - assert "MPEG-TS" in protocol - assert "SHIN/1" in protocol - assert "shin://connect" in protocol - assert "sourceInstanceId" in protocol - assert "slotId" in protocol - assert "slotLabel" in protocol - assert "pairingUrl" in protocol - assert "deviceName" in protocol - assert "reservedBy" in protocol - assert "latencyMs" in protocol - - -def test_docs_keep_python_receiver_as_developer_tool_only() -> None: - setup = read("docs/set-up.md") - assert "developer/debug path only" in setup - assert "not part of the normal user workflow" in setup - - def test_release_workflows_build_streaming_apk_and_plugin_package() -> None: android_workflow = read(".github/workflows/android.yml") obs_workflow = read(".github/workflows/obs-plugin-windows.yml") release_workflow = read(".github/workflows/release.yml") - release_docs = read("docs/release.md") plugin_builder = read("build_plugin.bat") gradle_properties = read("android/gradle.properties") @@ -294,12 +262,11 @@ def test_release_workflows_build_streaming_apk_and_plugin_package() -> None: assert "OPENSTREAM_RELEASE_KEY_ALIAS" in release_workflow assert "OPENSTREAM_RELEASE_KEY_PASSWORD" in release_workflow assert "openstream.versionName" in release_workflow - assert "android/gradle.properties" in release_docs assert "OPENSTREAM_SKIP_INSTALL=1" in obs_workflow assert "OPENSTREAM_PLUGIN_PACKAGE_DIR" in obs_workflow assert "openstream-obs-windows-x64.zip" in obs_workflow assert "gh release create" in release_workflow - assert "docs/release-notes-template.md" in release_workflow + assert "--generate-notes" in release_workflow assert "openstream-android.apk" in release_workflow assert "dist/openstream-android.apk" in release_workflow assert "openstream-android.apk.sha256" in release_workflow @@ -307,8 +274,6 @@ def test_release_workflows_build_streaming_apk_and_plugin_package() -> None: assert "sha256sum openstream-android.apk" in release_workflow assert "Public releases require all Android signing secrets" in release_workflow assert "openstream-obs-windows-x64.zip" in release_workflow - assert "never publishes a debug-signed fallback" in release_docs - assert "Android Signing Secrets" in release_docs assert "OPENSTREAM_SKIP_INSTALL" in plugin_builder assert "OPENSTREAM_PLUGIN_PACKAGE_DIR" in plugin_builder assert "Compress-Archive" in plugin_builder @@ -348,9 +313,9 @@ def test_legacy_android_and_restored_obs_metadata_are_explicit() -> None: installer = read("tools/installer/openstream-obs-plugin.iss") gradle_properties = read("android/gradle.properties") - assert "openstream.versionName=1.0.1" in gradle_properties - assert "project(openstream_obs_plugin VERSION 1.0.1" in cmake - assert '#define OpenStreamVersion "1.0.1"' in installer + assert "openstream.versionName=1.0.2" in gradle_properties + assert "project(openstream_obs_plugin VERSION 1.0.2" in cmake + assert '#define OpenStreamVersion "1.0.2"' in installer def test_openstream_linux_release_includes_native_obs_dock_dependencies() -> None: diff --git a/tests/test_srt_contract.py b/tests/test_srt_contract.py index 909f020e..969c82ba 100644 --- a/tests/test_srt_contract.py +++ b/tests/test_srt_contract.py @@ -1,3 +1,4 @@ +import re from pathlib import Path from _helpers import block_after @@ -14,6 +15,29 @@ NATIVE = ROOT / "android/app/src/main/cpp/openstream_srt.cpp" +def test_kotlin_native_methods_match_jni_exports(): + kotlin = KOTLIN.read_text(encoding="utf-8") + native = NATIVE.read_text(encoding="utf-8") + bridge = kotlin[kotlin.index("private object SrtNativeBridge") :] + kotlin_methods = set(re.findall(r"private external fun (native\w+)\(", bridge)) + native_methods = set( + re.findall( + r"Java_dev_openstream_app_stream_SrtNativeBridge_(native\w+)\(", + native, + ) + ) + + assert kotlin_methods == { + "nativeBeginSession", + "nativeConnect", + "nativeListen", + "nativeSendVideo", + "nativeSendAudio", + "nativeDisconnect", + } + assert native_methods == kotlin_methods + + def test_srt_address_fallback_uses_fresh_native_connects_and_honors_cancellation(): source = SRT_CLIENT.read_text(encoding="utf-8") @@ -43,20 +67,23 @@ def test_srt_address_fallback_uses_fresh_native_connects_and_honors_cancellation def test_srt_runtime_lifetime_is_process_scoped_and_disconnect_safe(): source = SRT_NATIVE.read_text(encoding="utf-8") - # NativeSender owns one libsrt runtime reference for its full lifetime. - # connect()/listen() must never acquire or release that global reference, - # so disconnect cannot tear the runtime down while setup is in progress. + # Runtime startup is delayed until JNI_OnLoad, after shared-library global + # constructors have completed. connect()/listen() never own the global ref. constructor_start = source.index("NativeSender()") + initialize_start = source.index("void initializeRuntime()", constructor_start) destructor_start = source.index("~NativeSender()", constructor_start) connect_start = source.index("bool connect(") - constructor = source[constructor_start:destructor_start] + constructor = source[constructor_start:initialize_start] destructor = source[destructor_start:connect_start] connect = source[connect_start : source.index("bool listen(")] listen = source[source.index("bool listen(") : source.index("bool sendNow(")] disconnect = source[source.index("void disconnect()") : source.index("private:", source.index("void disconnect()"))] - assert "srt_startup()" in constructor - assert "srtStarted_ = srt_startup() == 0;" in constructor + initialize = block_after(source, "void initializeRuntime()") + on_load = block_after(source, "JNI_OnLoad(") + assert "srt_startup()" not in constructor + assert "srtStarted_ = srt_startup() == 0;" in initialize + assert "g_state.sender.initializeRuntime();" in on_load assert "srt_cleanup()" not in constructor assert "srt_cleanup();" in destructor assert "srt_startup()" not in connect @@ -75,9 +102,11 @@ def test_srt_runtime_lifetime_is_process_scoped_and_disconnect_safe(): assert "Could not resolve SRT host\");\n disconnect();\n return false;" in connect assert "SRT connect failed: %s\", srt_getlasterror_str());\n disconnect();\n return false;" in connect assert "Could not create SRT listener socket\");\n disconnect();\n return false;" in listen - assert "SRT bind failed: %s\", srt_getlasterror_str());\n disconnect();\n return false;" in listen - assert "SRT listen failed: %s\", srt_getlasterror_str());\n disconnect();\n return false;" in listen - assert "SRT accept failed: %s\", srt_getlasterror_str());\n disconnect();\n return false;" in listen + assert "SRT bind failed: %s\", srt_getlasterror_str());\n closeListenerSocket(listenerSocket);\n return false;" in listen + assert "SRT listen failed: %s\", srt_getlasterror_str());\n closeListenerSocket(listenerSocket);\n return false;" in listen + assert "srt_epoll_uwait(epoll, &event, 1, kListenerAcceptPollMs)" in listen + assert "SRTO_RCVSYN" in listen + assert "closeListenerSocket(listenerSocket);" in listen def test_native_connect_and_listen_publish_only_current_generation(): @@ -106,7 +135,7 @@ def test_native_connect_and_listen_publish_only_current_generation(): def test_disconnect_invalidates_generation_before_native_teardown(): native = NATIVE.read_text() - body = block_after(native, "Java_dev_openstream_app_stream_SrtNativeBridge_disconnect") + body = block_after(native, "Java_dev_openstream_app_stream_SrtNativeBridge_nativeDisconnect") invalidate = "g_state.sender.advanceLifecycleGeneration(generation)" teardown = "g_state.sender.disconnect()" assert body.index(invalidate) < body.index(teardown) @@ -114,9 +143,9 @@ def test_disconnect_invalidates_generation_before_native_teardown(): def test_native_media_and_stale_teardown_are_generation_guarded(): native = NATIVE.read_text() - video = block_after(native, "Java_dev_openstream_app_stream_SrtNativeBridge_sendVideo") - audio = block_after(native, "Java_dev_openstream_app_stream_SrtNativeBridge_sendAudio") - disconnect = block_after(native, "Java_dev_openstream_app_stream_SrtNativeBridge_disconnect") + video = block_after(native, "Java_dev_openstream_app_stream_SrtNativeBridge_nativeSendVideo") + audio = block_after(native, "Java_dev_openstream_app_stream_SrtNativeBridge_nativeSendAudio") + disconnect = block_after(native, "Java_dev_openstream_app_stream_SrtNativeBridge_nativeDisconnect") generation_guard = ( "static_cast(session_generation) != " diff --git a/tools/build_android_apk.sh b/tools/build_android_apk.sh new file mode 100755 index 00000000..b705e193 --- /dev/null +++ b/tools/build_android_apk.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: tools/build_android_apk.sh [--release] [--install] + +Builds a shareable APK. Debug builds use this machine's Android debug key and +an automatically increasing version code. --release requires the four +OPENSTREAM_RELEASE_* signing variables used by the release workflow. Use it +to update an app installed from the public release. + +--install runs adb install -r after building. Android only updates an installed +app when the application ID and signing certificate match and the new version +code is at least as high. A different debug key or a debug/release switch +requires the original signing key; uninstalling removes the installed app data. +EOF +} + +release=false +install=false +for arg in "$@"; do + case "$arg" in + --release) release=true ;; + --install) install=true ;; + --help|-h) usage; exit 0 ;; + *) usage >&2; exit 2 ;; + esac +done + +repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +if [[ "$release" == true ]]; then + variant=Release + apk="$repo_dir/android/app/build/outputs/apk/release/app-release.apk" + echo "Building release APK with the configured release signing key." +else + variant=Debug + apk="$repo_dir/android/app/build/outputs/apk/debug/app-debug.apk" + echo "Building debug APK. It can update only an app signed with this machine's debug key." +fi + +(cd "$repo_dir/android" && ./gradlew ":app:assemble$variant") +echo "APK: $apk" + +if [[ "$install" == true ]]; then + if ! command -v adb >/dev/null 2>&1; then + echo "adb is required for --install (install Android SDK platform-tools)." >&2 + exit 1 + fi + if output="$(adb install -r "$apk" 2>&1)"; then + echo "$output" + else + echo "$output" >&2 + case "$output" in + *INSTALL_FAILED_UPDATE_INCOMPATIBLE*) echo "The installed app has a different signing key. Build with its original key." >&2 ;; + *INSTALL_FAILED_VERSION_DOWNGRADE*) echo "The installed app has a higher version code. Build a newer APK." >&2 ;; + esac + exit 1 + fi +fi diff --git a/tools/installer/openstream-obs-plugin.iss b/tools/installer/openstream-obs-plugin.iss index a57a2a9e..008e551d 100644 --- a/tools/installer/openstream-obs-plugin.iss +++ b/tools/installer/openstream-obs-plugin.iss @@ -11,7 +11,7 @@ #endif #ifndef OpenStreamVersion - #define OpenStreamVersion "1.0.1" + #define OpenStreamVersion "1.0.2" #endif [Setup] @@ -65,4 +65,3 @@ Type: files; Name: "{commonappdata}\obs-studio\plugins\openstream-obs\bin\64bit\ Type: files; Name: "{userappdata}\obs-studio\plugins\openstream-beta-obs\bin\64bit\openstream-obs.dll" Type: files; Name: "{commonappdata}\obs-studio\plugins\openstream-beta-obs\bin\64bit\openstream-obs.dll" Type: filesandordirs; Name: "{app}\data\obs-plugins\openstream-beta-obs" - diff --git a/website/README.md b/website/README.md deleted file mode 100644 index 026e60f1..00000000 --- a/website/README.md +++ /dev/null @@ -1,25 +0,0 @@ -# OpenStream Website - -The source for [openstream.pages.dev](https://openstream.pages.dev). The site is a static React application built with Vite. - -## Local development - -```powershell -npm install -npm run dev -``` - -## Production build - -```powershell -npm run build -``` - -Cloudflare Pages publishes the generated `dist/` directory. Before deploying, verify that the authenticated account owns the `openstream` Pages project: - -```powershell -npx wrangler pages project list -npx wrangler pages deploy dist --project-name openstream --branch main -``` - -Do not create a second Pages project as a substitute for the production project. Release links intentionally use GitHub's `latest/download` paths so future releases do not require a website edit. diff --git a/website/src/main.jsx b/website/src/main.jsx index 38992f46..6bbd59bb 100644 --- a/website/src/main.jsx +++ b/website/src/main.jsx @@ -10,7 +10,7 @@ const links = { release: "https://github.com/YashasVM/OpenStream/releases/latest", repo: "https://github.com/YashasVM/OpenStream", issues: "https://github.com/YashasVM/OpenStream/issues", - setup: "https://github.com/YashasVM/OpenStream/blob/main/docs/set-up.md", + setup: "#setup", }; function Header() { @@ -127,7 +127,6 @@ function Setup() {

Five clean moves.
Then live.

No desktop webcam client. No capture card. No screen-mirroring detour.

{steps.map(([n, title, copy]) =>
{n}

{title}

{copy}

)}
- Open the illustrated setup guide
); }