Skip to content
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
10e4f3f
Merge pull request #400 from OpenCloudGaming/dev
Kief5555 May 9, 2026
691965e
chore(release): prepare v0.3.9
github-actions[bot] May 9, 2026
95f4754
fix: specify shell for syncing package version in release workflow
Kief5555 May 9, 2026
5d56d79
Add Gradle wrapper files and update settings for Android project
Kief5555 May 14, 2026
f97710e
feat: Enhance streaming experience and UI updates
Kief5555 May 16, 2026
d0771a6
feat: Enhance session proxy settings and ad playback functionality
Kief5555 May 16, 2026
36149af
feat: Implement QR code generation and enhance streaming stats
Kief5555 May 16, 2026
97b644e
feat: Add functionality to remove completed ads from session state an…
Kief5555 May 16, 2026
02a7bfe
Refactor UI colors and enhance device login experience
Kief5555 May 16, 2026
05e8eff
feat: Enhance device code login support and improve session handling
Kief5555 May 16, 2026
fbd669d
feat: Replace loading skeleton with pulsing animation for game images
Kief5555 May 16, 2026
2eed4bd
feat: Enhance OpenNowViewModel with account switching and stream retu…
Kief5555 May 17, 2026
d2036a5
Potential fix for pull request finding
Kief5555 May 17, 2026
d525803
feat: Enhance stream resolution options and implement membership tier…
Kief5555 May 17, 2026
3b7d1c1
Merge branch 'android-native' of https://github.com/OpenCloudGaming/O…
Kief5555 May 17, 2026
49f6847
feat: Remove release artifacts and update .gitignore for cleaner builds
Kief5555 May 17, 2026
380a69a
feat: Update minimum SDK version and increment version code; enhance …
Kief5555 May 17, 2026
5963880
feat: Refactor token management to use milliseconds for refresh windo…
Kief5555 May 18, 2026
c722a8f
feat: Enhance WebRTC codec handling and input channel management
Kief5555 May 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,7 @@ jobs:
run: npm ci --prefer-offline --no-audit --progress=false

- name: Sync package version
shell: bash
run: node scripts/sync-release-version.mjs "$RELEASE_VERSION"

- name: Verify macOS Cargo network trust
Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ gfn_tokens.json

# Test files
test/
!android/app/src/test/
!android/app/src/test/**

package-lock.json
opennow-stable/package-lock.json
Expand All @@ -56,3 +58,6 @@ release-notes.md
result
.deriveddata/
.deriveddata-device/

/android/app/.cxx
/android/app/release/
8 changes: 8 additions & 0 deletions android/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.gradle/
.idea/
build/
local.properties
captures/
*.iml
app/build/
app/release/
17 changes: 17 additions & 0 deletions android/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# OpenNOW Native Android

This folder is a standalone Android Studio project for the native Kotlin / Jetpack Compose OpenNOW target.

Open it from Android Studio with **File > Open > `OpenNOW/android`**. Android Studio will install/use the required Gradle, Android SDK, CMake, and NDK components from the project configuration.

## Build Targets

- `:app:assembleDebug` builds a debug APK.
- `:app:assembleRelease` builds a release APK after signing config is added locally.

## Runtime Notes

- UI is native Compose.
- GFN auth, catalog, subscription, CloudMatch session creation/polling/claim/stop, signaling, and input packet behavior are implemented in Kotlin from the Electron project contracts.
- Streaming uses Android WebRTC plus hardware MediaCodec probing. The bundled `opennow_native` JNI library exposes native runtime diagnostics and keeps the NDK/CMake path wired for media-sensitive code.
- Queue ad metadata is preserved in `SessionInfo`; ad playback can use the included Media3 dependency when the server returns an ad media URL.
105 changes: 105 additions & 0 deletions android/app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.plugin.compose")
id("org.jetbrains.kotlin.plugin.serialization")
}

android {
namespace = "com.opencloudgaming.opennow"
compileSdk = 36

defaultConfig {
applicationId = "com.opencloudgaming.opennow"
minSdk = 26
targetSdk = 36
versionCode = 2
versionName = "0.3.9-native"

ndk {
abiFilters += listOf("arm64-v8a", "armeabi-v7a", "x86_64")
}

externalNativeBuild {
cmake {
cppFlags += listOf("-std=c++17", "-fexceptions", "-frtti")
}
}
}

buildTypes {
debug {
isMinifyEnabled = false
}
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro",
)
}
}

buildFeatures {
compose = true
buildConfig = true
}

externalNativeBuild {
cmake {
path = file("src/main/cpp/CMakeLists.txt")
version = "3.22.1"
}
}

packaging {
jniLibs {
useLegacyPackaging = false
}
resources {
excludes += setOf(
"META-INF/AL2.0",
"META-INF/LGPL2.1",
"META-INF/LICENSE*",
"META-INF/NOTICE*",
)
}
}
}

kotlin {
jvmToolchain(17)
}

dependencies {
implementation(platform("androidx.compose:compose-bom:2026.04.01"))
androidTestImplementation(platform("androidx.compose:compose-bom:2026.04.01"))

implementation("androidx.activity:activity-compose:1.11.0")
implementation("androidx.browser:browser:1.9.0")
implementation("androidx.compose.foundation:foundation")
implementation("androidx.compose.material3:material3")
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.10.0")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.10.0")
implementation("androidx.media3:media3-common:1.9.0")
implementation("androidx.media3:media3-exoplayer:1.9.0")
implementation("androidx.media3:media3-ui:1.9.0")

implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:4.12.0")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("io.github.webrtc-sdk:android:144.7559.05")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.1")

debugImplementation("androidx.compose.ui:ui-tooling")
debugImplementation("androidx.compose.ui:ui-test-manifest")

testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.3.0")
androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0")
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
}
6 changes: 6 additions & 0 deletions android/app/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-keep class org.webrtc.** { *; }
-keep class org.jni_zero.** { *; }
-keep class kotlinx.serialization.** { *; }
-keepclassmembers class com.opencloudgaming.opennow.** {
@kotlinx.serialization.Serializable *;
}
Binary file added android/app/release/app-release.apk
Binary file not shown.
Binary file not shown.
Binary file not shown.
37 changes: 37 additions & 0 deletions android/app/release/output-metadata.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"version": 3,
"artifactType": {
"type": "APK",
"kind": "Directory"
},
"applicationId": "com.opencloudgaming.opennow",
"variantName": "release",
"elements": [
{
"type": "SINGLE",
"filters": [],
"attributes": [],
"versionCode": 1,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[🟡 Medium] [🔵 Bug]

The checked-in release metadata declares versionCode: 1 while app config sets versionCode = 2, so the packaged artifact metadata is stale/inconsistent with the source build configuration. ```json
// android/app/release/output-metadata.json
"versionCode": 1,
"versionName": "0.3.9-native",
"outputFile": "app-release.apk"


```suggestion
      "versionCode": 2,

"versionName": "0.3.9-native",
"outputFile": "app-release.apk"
}
],
"elementType": "File",
"baselineProfiles": [
{
"minApi": 28,
"maxApi": 30,
"baselineProfiles": [
"baselineProfiles/1/app-release.dm"
]
},
{
"minApi": 31,
"maxApi": 2147483647,
"baselineProfiles": [
"baselineProfiles/0/app-release.dm"
]
}
],
"minSdkVersionForDexing": 26
}
78 changes: 78 additions & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

<uses-feature
android:name="android.hardware.gamepad"
android:required="false" />
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false" />
<uses-feature
android:name="android.hardware.faketouch"
android:required="false" />
<uses-feature
android:name="android.software.leanback"
android:required="false" />
<uses-feature
android:name="android.hardware.microphone"
android:required="false" />

<application
android:allowBackup="false"
android:banner="@drawable/opennow_banner"
android:hardwareAccelerated="true"
android:icon="@drawable/opennow_icon"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:resizeableActivity="true"
android:roundIcon="@drawable/opennow_icon"
android:supportsRtl="true"
android:theme="@style/Theme.OpenNOW">
<activity
android:name=".MainActivity"
android:configChanges="keyboard|keyboardHidden|navigation|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
android:exported="true"
android:launchMode="singleTask"
android:screenOrientation="fullSensor"
android:supportsPictureInPicture="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="opennow" android:host="launch" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" android:host="localhost" android:port="2259" />
<data android:scheme="http" android:host="localhost" android:port="6460" />
<data android:scheme="http" android:host="localhost" android:port="7119" />
<data android:scheme="http" android:host="localhost" android:port="8870" />
<data android:scheme="http" android:host="localhost" android:port="9096" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" android:host="127.0.0.1" android:port="2259" />
<data android:scheme="http" android:host="127.0.0.1" android:port="6460" />
<data android:scheme="http" android:host="127.0.0.1" android:port="7119" />
<data android:scheme="http" android:host="127.0.0.1" android:port="8870" />
<data android:scheme="http" android:host="127.0.0.1" android:port="9096" />
</intent-filter>
</activity>
</application>
</manifest>
16 changes: 16 additions & 0 deletions android/app/src/main/cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
cmake_minimum_required(VERSION 3.22.1)

project(opennow_native)

add_library(opennow_native SHARED opennow_native.cpp)

find_library(log-lib log)
find_library(android-lib android)
find_library(mediandk-lib mediandk)

target_link_libraries(
opennow_native
${android-lib}
${log-lib}
${mediandk-lib}
)
18 changes: 18 additions & 0 deletions android/app/src/main/cpp/opennow_native.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#include <jni.h>
#include <media/NdkMediaCodec.h>
#include <media/NdkMediaFormat.h>
#include <sstream>
#include <string>

extern "C" JNIEXPORT jstring JNICALL
Java_com_opencloudgaming_opennow_NativeCodecProbe_nativeRuntimeSummary(JNIEnv *env, jobject) {
std::ostringstream out;
out << "{";
out << "\"nativeLibrary\":\"opennow_native\",";
out << "\"mediaNdk\":true,";
out << "\"rtpPacketSize\":1140,";
out << "\"inputProtocolVersion\":3";
out << "}";
const std::string value = out.str();
return env->NewStringUTF(value.c_str());
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.opencloudgaming.opennow

private val READY_SESSION_STATUSES = setOf(2, 3)

internal fun isSessionAdsRequired(adState: SessionAdState?): Boolean =
adState?.sessionAdsRequired ?: (adState?.isAdsRequired == true)

internal fun sessionAdItems(adState: SessionAdState?): List<SessionAdInfo> =
adState?.sessionAds?.takeIf { it.isNotEmpty() } ?: adState?.ads.orEmpty()

internal fun shouldWaitForQueueAdPlayback(adState: SessionAdState?): Boolean =
isSessionAdsRequired(adState) && sessionAdItems(adState).isNotEmpty()

internal fun mergeQueueAdState(
previous: SessionAdState?,
next: SessionAdState?,
preserveMissingAdState: Boolean = true,
): SessionAdState? {
if (next == null) return if (preserveMissingAdState) previous else null
val shouldRestorePreviousAds =
preserveMissingAdState &&
isSessionAdsRequired(next) &&
next.serverSentEmptyAds &&
sessionAdItems(next).isEmpty() &&
sessionAdItems(previous).isNotEmpty()

return if (shouldRestorePreviousAds) {
next.copy(
sessionAds = sessionAdItems(previous),
ads = previous?.ads?.takeIf { it.isNotEmpty() } ?: sessionAdItems(previous),
)
} else {
next
}
}

internal fun mergeQueueSessionState(
previous: SessionInfo,
next: SessionInfo,
preserveMissingAdState: Boolean = true,
): SessionInfo {
if (next.status in READY_SESSION_STATUSES) return next
return next.copy(
adState = mergeQueueAdState(previous.adState, next.adState, preserveMissingAdState),
mediaConnectionInfo = next.mediaConnectionInfo ?: previous.mediaConnectionInfo,
)
}

internal fun removeSessionAdItem(adState: SessionAdState?, adId: String): SessionAdState? {
if (adState == null) return null
return adState.copy(
sessionAds = adState.sessionAds.filterNot { it.adId == adId },
ads = adState.ads.filterNot { it.adId == adId },
serverSentEmptyAds = false,
)
}

internal fun removeSessionAdItem(session: SessionInfo, adId: String): SessionInfo =
session.copy(adState = removeSessionAdItem(session.adState, adId))
Loading
Loading