From 104c182fdce4c91755e2e6da5bf48b05a2919819 Mon Sep 17 00:00:00 2001 From: ARST113 <96774211+ARST113@users.noreply.github.com> Date: Thu, 27 Aug 2026 11:54:23 +0300 Subject: [PATCH 01/52] Prepare Just+ transition modules for v0.0.15 --- settings.gradle.kts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/settings.gradle.kts b/settings.gradle.kts index 79d6616..b30e5eb 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -21,4 +21,19 @@ dependencyResolutionManagement { rootProject.name = "DDD Video Player" include(":app") - +include(":legacy") + +// Just Player 2.1 transition build. The upstream source is fetched by +// scripts/prepare-justplus.sh immediately before Gradle is invoked in CI. +if (providers.gradleProperty("useJustPlus").orNull == "true") { + val upstream = file(".justplus-upstream") + require(upstream.isDirectory) { + "Missing .justplus-upstream. Run scripts/prepare-justplus.sh first." + } + include(":justplusapp") + project(":justplusapp").projectDir = file(".justplus-upstream/app") + include(":doubletapplayerview") + project(":doubletapplayerview").projectDir = file(".justplus-upstream/doubletapplayerview") + include(":android-file-chooser") + project(":android-file-chooser").projectDir = file(".justplus-upstream/android-file-chooser") +} From e9aadec94d768c699e69ab1d22a770e3575a89fb Mon Sep 17 00:00:00 2001 From: ARST113 <96774211+ARST113@users.noreply.github.com> Date: Thu, 27 Aug 2026 11:54:32 +0300 Subject: [PATCH 02/52] Align Gradle settings with embedded Just+ modules --- gradle.properties | 36 ++++++++++++++++-------------------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/gradle.properties b/gradle.properties index 20e2a01..fc1cc19 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,23 +1,19 @@ # Project-wide Gradle settings. -# IDE (e.g. Android Studio) users: -# Gradle settings configured through the IDE *will override* -# any settings specified in this file. -# For more details on how to configure your build environment visit -# http://www.gradle.org/docs/current/userguide/build_environment.html -# Specifies the JVM arguments used for the daemon process. -# The setting is particularly useful for tweaking memory settings. -org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 -# When configured, Gradle will run in incubating parallel mode. -# This option should only be used with decoupled projects. For more details, visit -# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects -# org.gradle.parallel=true -# AndroidX package structure to make it clearer which packages are bundled with the -# Android operating system, and which are packaged with your app's APK -# https://developer.android.com/topic/libraries/support-library/androidx-rn +org.gradle.jvmargs=-Xmx3072m -Dfile.encoding=UTF-8 android.useAndroidX=true -# Kotlin code style for this project: "official" or "obsolete": +android.enableJetifier=true kotlin.code.style=official -# Enables namespacing of each library's R class so that its R class includes only the -# resources declared in the library itself and none from the library's dependencies, -# thereby reducing the size of the R class for that library -android.nonTransitiveRClass=true \ No newline at end of file + +# Just+ uses local Media3 AARs and Java switch statements over resource IDs. +# Keep the same resource/classpath behavior as upstream Just+ for the transition build. +android.useFullClasspathForDexingTransform=true +android.nonTransitiveRClass=false +android.nonFinalResIds=false + +# android-file-chooser is included as a subproject of the Just+ source tree. +VERSION_NAME=1.2.0 +VERSION_CODE=102000 +ANDROID_COMPILE_SDK_VERSION=34 +ANDROID_MIN_SDK_VERSION=21 +ANDROID_TARGET_SDK_VERSION=34 +JAVA_COMPATIBILITY=VERSION_1_8 From 0d0c33ae0664492078a5188340d231583498fb02 Mon Sep 17 00:00:00 2001 From: ARST113 <96774211+ARST113@users.noreply.github.com> Date: Thu, 27 Aug 2026 11:55:05 +0300 Subject: [PATCH 03/52] Add legacy DDD library for HEVC fallback --- legacy/build.gradle.kts | 130 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 legacy/build.gradle.kts diff --git a/legacy/build.gradle.kts b/legacy/build.gradle.kts new file mode 100644 index 0000000..56b0d8d --- /dev/null +++ b/legacy/build.gradle.kts @@ -0,0 +1,130 @@ +import org.gradle.api.tasks.Sync + +plugins { + alias(libs.plugins.kotlin.android) + alias(libs.plugins.ksp) +} +apply(plugin = "com.android.library") + +val nativeRoot = rootProject.file("native") +val ffmpegAbis = listOf("arm64-v8a") + +val syncLegacyFfmpegLibs = tasks.register("syncLegacyFfmpegLibs") { + description = "Prepare DDD FFmpeg libraries for the legacy HEVC fallback" + into(layout.buildDirectory.dir("ffmpegJniLibs")) + ffmpegAbis.forEach { abi -> + val stripped = File(nativeRoot, "prebuilt/ffmpeg/$abi/stripped") + val full = File(nativeRoot, "prebuilt/ffmpeg/$abi/lib") + from(if (stripped.isDirectory) stripped else full) { + include("*.so") + into(abi) + } + } +} + +android { + namespace = "top.rootu.dddplayer" + compileSdk = 36 + ndkVersion = "27.0.12077973" + + defaultConfig { + minSdk = 23 + buildConfigField("String", "VERSION_NAME", "\"0.0.15-legacy\"") + buildConfigField("int", "VERSION_CODE", "1500000") + ndk { + abiFilters += ffmpegAbis + } + externalNativeBuild { + cmake { + arguments += listOf( + "-DANDROID_STL=c++_shared", + "-DCMAKE_TRY_COMPILE_TARGET_TYPE=STATIC_LIBRARY" + ) + } + } + } + + externalNativeBuild { + cmake { + path = File(nativeRoot, "ddd_engine/CMakeLists.txt") + version = "3.22.1" + } + } + + sourceSets { + getByName("main") { + java.srcDir(rootProject.file("app/src/main/java")) + res.srcDir(rootProject.file("app/src/main/res")) + manifest.srcFile("src/main/AndroidManifest.xml") + jniLibs.srcDir(layout.buildDirectory.dir("ffmpegJniLibs")) + } + } + + buildFeatures { + buildConfig = true + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + packaging { + jniLibs { + keepDebugSymbols += "*/*/libav*_ddd.so" + keepDebugSymbols += "*/*/libsw*_ddd.so" + } + } + + lint { + disable.add("MissingTranslation") + disable.add("UnsafeOptInUsageError") + } +} + +tasks.named("preBuild") { dependsOn(syncLegacyFfmpegLibs) } + +kotlin { + compilerOptions { + jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8) + } +} + +dependencies { + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.leanback) + implementation(libs.androidx.appcompat) + implementation(libs.material) + implementation(libs.androidx.constraintlayout) + implementation(libs.androidx.fragment.ktx) + implementation(libs.okhttp) + implementation(libs.gson) + implementation(libs.androidx.room.runtime) + implementation(libs.androidx.room.ktx) + implementation(libs.androidx.ui.graphics) + implementation(libs.coil) + ksp(libs.androidx.room.compiler) + + // Legacy DDD code compiles against the same Media3 API generation used by Just+. + // The final APK receives ExoPlayer/UI from Just+'s patched local AARs, so these + // dependencies are compileOnly to avoid duplicate androidx.media3 classes. + val media3 = "1.11.0-beta01" + compileOnly("androidx.media3:media3-common:$media3") + compileOnly("androidx.media3:media3-container:$media3") + compileOnly("androidx.media3:media3-datasource:$media3") + compileOnly("androidx.media3:media3-datasource-okhttp:$media3") + compileOnly("androidx.media3:media3-decoder:$media3") + compileOnly("androidx.media3:media3-extractor:$media3") + compileOnly("androidx.media3:media3-session:$media3") + compileOnly("androidx.media3:media3-exoplayer-hls:$media3") + compileOnly("androidx.media3:media3-exoplayer-dash:$media3") + compileOnly("androidx.media3:media3-exoplayer-smoothstreaming:$media3") + compileOnly("androidx.media3:media3-exoplayer-rtsp:$media3") + compileOnly(fileTree(mapOf( + "dir" to rootProject.file(".justplus-upstream/app/libs"), + "include" to listOf("lib-*.aar") + ))) + + // Kept only for the legacy fallback activity. Normal playback stays entirely in Just+. + implementation("org.videolan.android:libvlc-all:3.7.5") +} From 9c411af2d749092aa3c100e29623f25aad4ba4c3 Mon Sep 17 00:00:00 2001 From: ARST113 <96774211+ARST113@users.noreply.github.com> Date: Thu, 27 Aug 2026 11:55:27 +0300 Subject: [PATCH 04/52] Declare legacy DDD fallback activities --- legacy/src/main/AndroidManifest.xml | 30 +++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 legacy/src/main/AndroidManifest.xml diff --git a/legacy/src/main/AndroidManifest.xml b/legacy/src/main/AndroidManifest.xml new file mode 100644 index 0000000..c5a1980 --- /dev/null +++ b/legacy/src/main/AndroidManifest.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + From 79d5d9fe5833168c47e5c132498b3e0fbd9b0d84 Mon Sep 17 00:00:00 2001 From: ARST113 <96774211+ARST113@users.noreply.github.com> Date: Thu, 27 Aug 2026 11:56:44 +0300 Subject: [PATCH 05/52] Bridge Just+ playback into DDD local and remote sync --- .../dddplayer/compat/JustPlusBridgeInterop.kt | 347 ++++++++++++++++++ 1 file changed, 347 insertions(+) create mode 100644 app/src/main/java/top/rootu/dddplayer/compat/JustPlusBridgeInterop.kt diff --git a/app/src/main/java/top/rootu/dddplayer/compat/JustPlusBridgeInterop.kt b/app/src/main/java/top/rootu/dddplayer/compat/JustPlusBridgeInterop.kt new file mode 100644 index 0000000..1c2e5f6 --- /dev/null +++ b/app/src/main/java/top/rootu/dddplayer/compat/JustPlusBridgeInterop.kt @@ -0,0 +1,347 @@ +package top.rootu.dddplayer.compat + +import android.app.Activity +import android.content.Intent +import android.net.Uri +import android.os.Handler +import android.os.Looper +import android.util.Log +import androidx.media3.common.C +import androidx.media3.common.MediaItem as Media3MediaItem +import androidx.media3.common.PlaybackException +import androidx.media3.common.Player +import okhttp3.OkHttpClient +import top.rootu.dddplayer.bridge.BridgeConfig +import top.rootu.dddplayer.bridge.BridgeDispatcher +import top.rootu.dddplayer.bridge.BridgeEvent +import top.rootu.dddplayer.bridge.BridgeMediaItem +import top.rootu.dddplayer.bridge.BridgeMode +import top.rootu.dddplayer.bridge.BridgeTransport +import top.rootu.dddplayer.bridge.BroadcastTransport +import top.rootu.dddplayer.bridge.CompositeTransport +import top.rootu.dddplayer.bridge.DddSyncContext +import top.rootu.dddplayer.bridge.DddSyncRemoteClient +import top.rootu.dddplayer.bridge.LocalBridgeManager +import top.rootu.dddplayer.bridge.LocalBridgeTransport +import top.rootu.dddplayer.model.MediaItem +import top.rootu.dddplayer.utils.IntentUtils +import java.util.WeakHashMap +import java.util.concurrent.TimeUnit + +/** + * Compatibility layer used by the Just+ based 0.0.15 transition build. + * + * Playback remains owned by Just+/Media3. This object only observes Player events and feeds the + * existing DDD broadcast/local bridge and DddSync server contracts. Keeping this in the legacy + * library means the Just+ fork needs only two small hooks: attach after player creation and detach + * from Activity.onDestroy(). + */ +object JustPlusBridgeInterop { + const val EXTRA_ORIGINAL_URI = "ddd_original_uri" + const val EXTRA_FORCE_LEGACY = "ddd_force_legacy" + + private val main = Handler(Looper.getMainLooper()) + private val sessions = WeakHashMap() + + @JvmStatic + @Synchronized + fun attach(activity: Activity, launchIntent: Intent, player: Player) { + val existing = sessions[activity] + if (existing != null) { + existing.replacePlayer(player) + return + } + + val intent = restoreOriginalIntent(launchIntent) + val config = runCatching { IntentUtils.parseBridgeConfig(intent) } + .getOrElse { + Log.w(TAG, "Bridge config parse failed", it) + BridgeConfig() + } + val parsed = runCatching { IntentUtils.parseIntent(activity, intent) } + .getOrElse { + Log.w(TAG, "DDD sync intent parse failed", it) + emptyList() to 0 + } + val items = parsed.first + + val transports = mutableListOf() + if (config.enabled && (config.mode == BridgeMode.BROADCAST || config.mode == BridgeMode.BOTH)) { + transports += BroadcastTransport(activity.applicationContext, config) + } + if (config.enabled && (config.mode == BridgeMode.LOCAL || config.mode == BridgeMode.BOTH)) { + val store = LocalBridgeManager.startOrReuse(config) + transports += LocalBridgeTransport(config, store) + } + val dispatcher = when (transports.size) { + 0 -> null + 1 -> BridgeDispatcher(config, transports.first()) + else -> BridgeDispatcher(config, CompositeTransport(transports)) + } + + val hasRemote = items.any { it.dddSyncContext?.enabled == true } + val remoteClient = if (hasRemote) { + DddSyncRemoteClient( + OkHttpClient.Builder() + .connectTimeout(3, TimeUnit.SECONDS) + .readTimeout(5, TimeUnit.SECONDS) + .writeTimeout(5, TimeUnit.SECONDS) + .build() + ) + } else null + + val session = Session(activity, config, dispatcher, items, remoteClient, player) + sessions[activity] = session + session.start() + } + + @JvmStatic + @Synchronized + fun detach(activity: Activity, reason: String = "activity_destroyed") { + sessions.remove(activity)?.stop(reason) + } + + private fun restoreOriginalIntent(intent: Intent): Intent { + val original = intent.getStringExtra(EXTRA_ORIGINAL_URI)?.takeIf { it.isNotBlank() } + ?: return Intent(intent) + return Intent(intent).setData(runCatching { Uri.parse(original) }.getOrNull() ?: intent.data) + } + + private class Session( + private val activity: Activity, + private val config: BridgeConfig, + private val dispatcher: BridgeDispatcher?, + private val items: List, + private val remoteClient: DddSyncRemoteClient?, + player: Player + ) { + private var player: Player = player + private var listener: Player.Listener? = null + private var lastPosition = 0L + private var lastIndex = player.currentMediaItemIndex.coerceAtLeast(0) + private var ended = false + private var stopped = false + + private val tick = object : Runnable { + override fun run() { + if (stopped) return + emitPosition("tick") + main.postDelayed(this, config.positionIntervalMs.coerceAtLeast(250L)) + } + } + + fun start() { + installListener() + val now = System.currentTimeMillis() + emit( + BridgeEvent.SessionStarted( + sessionId = sessionId(), + ts = now, + uri = currentUri(), + title = currentTitle(), + playlistSize = player.mediaItemCount, + startIndex = player.currentMediaItemIndex.coerceAtLeast(0), + startPosition = player.currentPosition.coerceAtLeast(0L), + currentItem = currentBridgeItem() + ) + ) + lastPosition = player.currentPosition.coerceAtLeast(0L) + if (config.enabled || remoteClient != null) main.post(tick) + } + + fun replacePlayer(next: Player) { + listener?.let { runCatching { player.removeListener(it) } } + player = next + ended = false + installListener() + } + + fun stop(reason: String) { + if (stopped) return + stopped = true + main.removeCallbacks(tick) + listener?.let { runCatching { player.removeListener(it) } } + emit( + BridgeEvent.SessionFinished( + sessionId = sessionId(), + ts = System.currentTimeMillis(), + uri = currentUri(), + position = player.currentPosition.coerceAtLeast(0L), + duration = durationOrNull(), + endBy = reason, + windowIndex = player.currentMediaItemIndex.coerceAtLeast(0), + playlistSize = player.mediaItemCount, + title = currentTitle() + ) + ) + if (config.mode == BridgeMode.LOCAL || config.mode == BridgeMode.BOTH) { + LocalBridgeManager.stopDelayed() + } + } + + private fun installListener() { + val l = object : Player.Listener { + override fun onIsPlayingChanged(isPlaying: Boolean) { + emitState(if (isPlaying) "playing" else "paused") + } + + override fun onPlaybackStateChanged(playbackState: Int) { + emitState( + when (playbackState) { + Player.STATE_BUFFERING -> "buffering" + Player.STATE_READY -> "ready" + Player.STATE_ENDED -> "ended" + else -> "idle" + } + ) + if (playbackState == Player.STATE_ENDED && !ended) { + ended = true + emit( + BridgeEvent.PlaybackEnded( + sessionId = sessionId(), + ts = System.currentTimeMillis(), + uri = currentUri(), + windowIndex = player.currentMediaItemIndex.coerceAtLeast(0), + playlistSize = player.mediaItemCount, + title = currentTitle(), + position = player.currentPosition.coerceAtLeast(0L), + duration = durationOrNull() + ) + ) + } + } + + override fun onMediaItemTransition(mediaItem: Media3MediaItem?, reason: Int) { + ended = false + lastIndex = player.currentMediaItemIndex.coerceAtLeast(0) + emit( + BridgeEvent.PlaylistItemChanged( + sessionId = sessionId(), + ts = System.currentTimeMillis(), + uri = currentUri(), + windowIndex = lastIndex, + playlistSize = player.mediaItemCount, + title = currentTitle(), + reason = "media_transition_$reason", + position = player.currentPosition.coerceAtLeast(0L), + duration = durationOrNull(), + hasPrevious = player.hasPreviousMediaItem(), + hasNext = player.hasNextMediaItem(), + currentItem = currentBridgeItem() + ) + ) + } + + override fun onPositionDiscontinuity( + oldPosition: Player.PositionInfo, + newPosition: Player.PositionInfo, + reason: Int + ) { + if (reason == Player.DISCONTINUITY_REASON_SEEK) { + emit( + BridgeEvent.SeekCompleted( + sessionId = sessionId(), + ts = System.currentTimeMillis(), + uri = currentUri(), + fromPosition = oldPosition.positionMs, + toPosition = newPosition.positionMs, + windowIndex = player.currentMediaItemIndex.coerceAtLeast(0) + ) + ) + } + lastPosition = newPosition.positionMs.coerceAtLeast(0L) + } + + override fun onPlayerError(error: PlaybackException) { + emit( + BridgeEvent.Error( + sessionId = sessionId(), + ts = System.currentTimeMillis(), + uri = currentUri(), + code = error.errorCodeName, + errorCode = error.errorCode, + message = error.message, + windowIndex = player.currentMediaItemIndex.coerceAtLeast(0), + position = player.currentPosition.coerceAtLeast(0L), + duration = durationOrNull(), + bufferedPosition = player.bufferedPosition.coerceAtLeast(0L), + bufferedPercentage = player.bufferedPercentage, + playlistSize = player.mediaItemCount, + title = currentTitle(), + fatal = true + ) + ) + } + } + listener = l + player.addListener(l) + } + + private fun emitState(reason: String) { + emit( + BridgeEvent.PlaybackStateChanged( + sessionId = sessionId(), + ts = System.currentTimeMillis(), + uri = currentUri(), + isPlaying = player.isPlaying, + isBuffering = player.playbackState == Player.STATE_BUFFERING, + position = player.currentPosition.coerceAtLeast(0L), + duration = durationOrNull(), + windowIndex = player.currentMediaItemIndex.coerceAtLeast(0), + title = currentTitle(), + reason = reason + ) + ) + } + + private fun emitPosition(reason: String) { + val position = player.currentPosition.coerceAtLeast(0L) + lastPosition = position + emit( + BridgeEvent.PositionTick( + sessionId = sessionId(), + ts = System.currentTimeMillis(), + uri = currentUri(), + position = position, + duration = durationOrNull(), + bufferedPosition = player.bufferedPosition.coerceAtLeast(0L), + bufferedPercentage = player.bufferedPercentage, + windowIndex = player.currentMediaItemIndex.coerceAtLeast(0), + title = currentTitle(), + reason = reason + ) + ) + } + + private fun emit(event: BridgeEvent) { + dispatcher?.emit(event) + currentSyncContext()?.let { context -> remoteClient?.send(event, context) } + } + + private fun currentItem(): MediaItem? = items.getOrNull(player.currentMediaItemIndex.coerceAtLeast(0)) + + private fun currentSyncContext(): DddSyncContext? = currentItem()?.dddSyncContext + + private fun sessionId(): String? = config.sessionId ?: currentSyncContext()?.sessionId + + private fun currentUri(): String? = player.currentMediaItem?.localConfiguration?.uri?.toString() + ?: currentItem()?.uri?.toString() + + private fun currentTitle(): String? = player.currentMediaItem?.mediaMetadata?.title?.toString() + ?: currentItem()?.title + + private fun durationOrNull(): Long? = player.duration.takeIf { it > 0L && it != C.TIME_UNSET } + + private fun currentBridgeItem(): BridgeMediaItem? { + val item = currentItem() + return BridgeMediaItem( + uri = currentUri(), + title = currentTitle(), + filename = item?.filename, + source = "justplus" + ) + } + } + + private const val TAG = "JustPlus/DDDInterop" +} From 610476f520d9a6157d9a3e3db78ff75cd69f37c4 Mon Sep 17 00:00:00 2001 From: ARST113 <96774211+ARST113@users.noreply.github.com> Date: Thu, 27 Aug 2026 11:58:06 +0300 Subject: [PATCH 06/52] Add reproducible Just+ v1.2.0 transition patcher --- scripts/prepare-justplus.sh | 200 ++++++++++++++++++++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 scripts/prepare-justplus.sh diff --git a/scripts/prepare-justplus.sh b/scripts/prepare-justplus.sh new file mode 100644 index 0000000..7cd5400 --- /dev/null +++ b/scripts/prepare-justplus.sh @@ -0,0 +1,200 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +UPSTREAM="$ROOT/.justplus-upstream" + +rm -rf "$UPSTREAM" +git clone --depth 1 --branch v1.2.0 https://github.com/just-plus-player/just-plus-player.git "$UPSTREAM" + +export DDD_ROOT="$ROOT" +python3 <<'PY' +from pathlib import Path +import os +import re + +root = Path(os.environ["DDD_ROOT"]) +up = root / ".justplus-upstream" + +# ---- Patch Just+ Gradle identity/signing/dependency ---- +build = up / "app/build.gradle" +s = build.read_text() +s = s.replace('applicationId "com.justplus.player"', 'applicationId "top.rootu.dddplayer"') +s = s.replace('versionCode tagVersionCode', 'versionCode (project.findProperty("versionCodeOverride") ?: "1500000").toString().toInteger()') +s = s.replace('versionName tagVersionName', 'versionName (project.findProperty("versionNameOverride") ?: "0.0.15").toString()') + +signing = ''' signingConfigs {\n release {\n def dddKeystore = rootProject.file("app/dddplayer-release.jks")\n if (dddKeystore.exists()) {\n storeFile dddKeystore\n storePassword System.getenv("ANDROID_KEYSTORE_PASSWORD")\n keyAlias System.getenv("ANDROID_KEY_ALIAS")\n keyPassword System.getenv("ANDROID_KEY_PASSWORD")\n }\n }\n }\n''' +if 'def dddKeystore' not in s: + s = s.replace(' buildTypes {\n', signing + ' buildTypes {\n', 1) +s = s.replace(' release {\n minifyEnabled false', + ' release {\n signingConfig signingConfigs.release\n minifyEnabled false', 1) +if "implementation project(path: ':legacy')" not in s: + s = s.replace('dependencies {\n', "dependencies {\n implementation project(path: ':legacy')\n", 1) +build.write_text(s) + +# Visible identity: Android package/signature stay DDD-compatible, UI becomes Just Player. +strings = up / "app/src/main/res/values/strings.xml" +s = strings.read_text() +s = re.sub(r'(]*>).*?()', r'\1Just Player 2.1\2', s, count=1) +strings.write_text(s) + +# Keep self-update/links on the same repository used by installed DDD 0.0.14. +for p in (up / "app/src/main/java").rglob("*.java"): + text = p.read_text() + changed = text.replace("just-plus-player/just-plus-player", "ARST113/dddplayer2") + if changed != text: + p.write_text(changed) + +# Future self-updates need package-installer permission under the inherited DDD application id. +manifest = up / "app/src/main/AndroidManifest.xml" +s = manifest.read_text() +if 'android.permission.REQUEST_INSTALL_PACKAGES' not in s: + s = s.replace('', + '\n ', 1) +manifest.write_text(s) + +# ---- Patch Just+ PlayerActivity: DDD bridge hooks + clean control metadata + HEVC fallback ---- +pa = up / "app/src/main/java/com/brouken/player/PlayerActivity.java" +s = pa.read_text() + +interop_import = 'import top.rootu.dddplayer.compat.JustPlusBridgeInterop;\n' +if interop_import not in s: + marker = 'import java.util.concurrent.TimeUnit;\n' + if marker not in s: + raise SystemExit('PlayerActivity import marker not found') + s = s.replace(marker, marker + '\n' + interop_import, 1) + +# DDD control query/fragment metadata is for the integration layer and must never reach TorrServer/CDN. +create_marker = ' super.onCreate(savedInstanceState);\n' +if 'sanitizeDddLaunchIntent();' not in s: + if create_marker not in s: + raise SystemExit('PlayerActivity onCreate marker not found') + s = s.replace(create_marker, create_marker + ' sanitizeDddLaunchIntent();\n', 1) + +# Attach after Just+ has installed its own listeners and analytics hooks. +attach_marker = ' player.addAnalyticsListener(playbackInfoListener);\n' +if 'JustPlusBridgeInterop.attach(this, getIntent(), player);' not in s: + if attach_marker not in s: + raise SystemExit('PlayerActivity attach marker not found') + s = s.replace(attach_marker, + attach_marker + ' JustPlusBridgeInterop.attach(this, getIntent(), player);\n', 1) + +# DDD internal headers contain JSON/control information and make strict media endpoints reject the request. +headers_marker = ''' if ("User-Agent".equalsIgnoreCase(name)) {\n userAgent = value;\n } else {\n headers.put(name, value);\n }''' +headers_replacement = ''' if (name.regionMatches(true, 0, "X-Lampa-DDD-", 0, "X-Lampa-DDD-".length())) {\n continue;\n }\n if ("User-Agent".equalsIgnoreCase(name)) {\n userAgent = value;\n } else {\n headers.put(name, value);\n }''' +if 'name.regionMatches(true, 0, "X-Lampa-DDD-"' not in s: + if headers_marker not in s: + raise SystemExit('PlayerActivity headers marker not found') + s = s.replace(headers_marker, headers_replacement, 1) + +# Prefer Just+'s DV->HEVC recovery. If plain HEVC itself is what failed, hand the exact launch +# to the preserved DDD/libavcodec pipeline. This covers RExt/4:2:2/4:4:4 10-bit files such as +# hvc1.4.10.H120.9C.08 without changing the working AV1/HDR/DV paths. +dv_marker = ''' if (error.errorCode == PlaybackException.ERROR_CODE_DECODING_FAILED\n && error instanceof ExoPlaybackException\n && recoverByForcingHevcForDolbyVision(error,\n ((ExoPlaybackException) error).rendererFormat)) {\n return;\n }''' +legacy_hook = dv_marker + '''\n if (error instanceof ExoPlaybackException\n && recoverByLaunchingLegacyHevc(error, ((ExoPlaybackException) error).rendererFormat)) {\n return;\n }''' +if 'recoverByLaunchingLegacyHevc(error' not in s: + if dv_marker not in s: + raise SystemExit('PlayerActivity DV recovery marker not found') + s = s.replace(dv_marker, legacy_hook, 1) + +methods = r''' + /** + * Transition-only compatibility path. Normal Main/Main10 HEVC stays in Just+/MediaCodec. + * When the selected Android HEVC decoder fails, the same Intent is opened by the preserved + * DDD activity, whose libavcodec fallback handles RExt 10-bit formats unsupported by MediaCodec. + */ + private boolean recoverByLaunchingLegacyHevc(PlaybackException error, @Nullable Format failingFormat) { + if (failingFormat == null || !MimeTypes.VIDEO_H265.equals(failingFormat.sampleMimeType) + || !isDecoderFailure(error)) { + return false; + } + final Intent current = getIntent(); + if (current != null && current.getBooleanExtra(JustPlusBridgeInterop.EXTRA_FORCE_LEGACY, false)) { + return false; + } + final Intent legacy = current == null ? new Intent() : new Intent(current); + final String originalUri = legacy.getStringExtra(JustPlusBridgeInterop.EXTRA_ORIGINAL_URI); + if (originalUri != null && !originalUri.isEmpty()) { + legacy.setData(Uri.parse(originalUri)); + } + legacy.setClassName(getPackageName(), "top.rootu.dddplayer.ui.PlayerActivity"); + legacy.putExtra(JustPlusBridgeInterop.EXTRA_FORCE_LEGACY, true); + if (player != null) { + legacy.putExtra("position", player.getCurrentPosition()); + } + JustPlusBridgeInterop.detach(this, "hevc_legacy_fallback"); + startActivity(legacy); + finish(); + return true; + } + + /** Strip DDD query/fragment control metadata while preserving it for bridge parsing. */ + private void sanitizeDddLaunchIntent() { + final Intent launch = getIntent(); + if (launch == null || launch.getData() == null) { + return; + } + final Uri data = launch.getData(); + final String original = data.toString(); + boolean changed = false; + String cleanQuery = null; + final String encodedQuery = data.getEncodedQuery(); + if (encodedQuery != null && !encodedQuery.isEmpty()) { + final ArrayList kept = new ArrayList<>(); + for (String part : encodedQuery.split("&")) { + final String rawKey = part.contains("=") ? part.substring(0, part.indexOf('=')) : part; + final String key = Uri.decode(rawKey); + if (key != null && key.toLowerCase(Locale.ROOT).startsWith("ddd_")) { + changed = true; + } else { + kept.add(part); + } + } + cleanQuery = kept.isEmpty() ? null : TextUtils.join("&", kept); + } + final String fragment = data.getFragment(); + final boolean dddFragment = fragment != null && fragment.toLowerCase(Locale.ROOT).contains("ddd_"); + changed |= dddFragment; + if (!changed) { + return; + } + final Uri.Builder builder = data.buildUpon(); + builder.encodedQuery(cleanQuery); + if (dddFragment) { + builder.fragment(null); + } + launch.putExtra(JustPlusBridgeInterop.EXTRA_ORIGINAL_URI, original); + launch.setData(builder.build()); + } + +''' +static_marker = ' private static boolean isDecoderFailure(PlaybackException error) {' +if 'private boolean recoverByLaunchingLegacyHevc' not in s: + if static_marker not in s: + raise SystemExit('PlayerActivity decoder failure marker not found') + s = s.replace(static_marker, methods + static_marker, 1) + +# Stop bridge observers/local socket with the Activity. attach() handles player rebuilds separately. +destroy_marker = ''' super.onDestroy();''' +if 'JustPlusBridgeInterop.detach(this, "activity_destroyed");' not in s: + # There is a single Activity.onDestroy() super call in this source. + if destroy_marker not in s: + raise SystemExit('PlayerActivity onDestroy marker not found') + s = s.replace(destroy_marker, + ' JustPlusBridgeInterop.detach(this, "activity_destroyed");\n' + destroy_marker, 1) + +pa.write_text(s) + +# ---- Patch old DDD PlayerActivity into a compatibility router ---- +legacy_pa = root / "app/src/main/java/top/rootu/dddplayer/ui/PlayerActivity.kt" +s = legacy_pa.read_text() +router_marker = ' super.onCreate(savedInstanceState)\n' +router = ''' super.onCreate(savedInstanceState)\n\n // 0.0.15 keeps this component name so existing Lampa integrations continue to work.\n // Normal launches are forwarded to Just+; only decoder fallback explicitly stays here.\n if (!intent.getBooleanExtra("ddd_force_legacy", false)) {\n runCatching {\n val forward = Intent(intent).apply {\n setClassName(packageName, "com.brouken.player.PlayerActivity")\n }\n startActivity(forward)\n finish()\n return\n }.onFailure { Log.w("DDDPlayer/Compat", "Just+ router failed, staying on legacy player", it) }\n }\n''' +if 'Just+ router failed' not in s: + if router_marker not in s: + raise SystemExit('Legacy PlayerActivity onCreate marker not found') + s = s.replace(router_marker, router, 1) +legacy_pa.write_text(s) + +print('Prepared Just+ v1.2.0 transition source') +PY From ff4800209063152c797d18872502ab7d0d67f6bf Mon Sep 17 00:00:00 2001 From: ARST113 <96774211+ARST113@users.noreply.github.com> Date: Thu, 27 Aug 2026 11:59:10 +0300 Subject: [PATCH 07/52] Build Just Player transition APK in CI --- .github/workflows/android-build.yml | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index da1b0f2..4959da1 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -18,6 +18,7 @@ jobs: uses: actions/checkout@v4 with: submodules: recursive + fetch-depth: 0 - name: Set up JDK 17 uses: actions/setup-java@v4 @@ -25,14 +26,30 @@ jobs: distribution: temurin java-version: 17 + - name: Prepare Just+ v1.2.0 transition source + run: bash scripts/prepare-justplus.sh + - name: Make gradlew executable run: chmod +x ./gradlew - - name: Build debug APK - run: ./gradlew assembleDebug --stacktrace + - name: Build Just Player 0.0.15 debug APK + run: | + ./gradlew :justplusapp:assembleLatestUniversalDebug \ + -PuseJustPlus=true \ + -PversionNameOverride=0.0.15 \ + -PversionCodeOverride=1500000 \ + --stacktrace + + - name: Collect debug APK + run: | + set -euo pipefail + mkdir -p build-artifacts + APK="$(find .justplus-upstream/app/build/outputs/apk -type f -name '*.apk' | grep -i latest | grep -i debug | head -n1)" + test -n "$APK" + cp "$APK" build-artifacts/just-player-v0.0.15-debug.apk - name: Upload debug APK uses: actions/upload-artifact@v4 with: - name: dddplayer-debug-apk - path: app/build/outputs/apk/debug/*.apk + name: just-player-v0.0.15-debug + path: build-artifacts/just-player-v0.0.15-debug.apk From 36fd786a738c1677c773e972636983577b1f0529 Mon Sep 17 00:00:00 2001 From: ARST113 <96774211+ARST113@users.noreply.github.com> Date: Thu, 27 Aug 2026 12:03:38 +0300 Subject: [PATCH 08/52] Fix Just+ v0.0.15 Gradle version literals --- scripts/fix-justplus-v015-gradle.sh | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 scripts/fix-justplus-v015-gradle.sh diff --git a/scripts/fix-justplus-v015-gradle.sh b/scripts/fix-justplus-v015-gradle.sh new file mode 100644 index 0000000..a285fc3 --- /dev/null +++ b/scripts/fix-justplus-v015-gradle.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +set -euo pipefail + +BUILD="$(cd "$(dirname "$0")/.." && pwd)/.justplus-upstream/app/build.gradle" +test -f "$BUILD" + +python3 - "$BUILD" <<'PY' +from pathlib import Path +import sys +p = Path(sys.argv[1]) +s = p.read_text() +s = s.replace( + 'versionCode (project.findProperty("versionCodeOverride") ?: "1500000").toString().toInteger()', + 'versionCode 1500000' +) +s = s.replace( + 'versionName (project.findProperty("versionNameOverride") ?: "0.0.15").toString()', + 'versionName "0.0.15"' +) +p.write_text(s) +PY From 499c8bb131a44ef70787386626f8e79cdd84551c Mon Sep 17 00:00:00 2001 From: ARST113 <96774211+ARST113@users.noreply.github.com> Date: Thu, 27 Aug 2026 12:03:53 +0300 Subject: [PATCH 09/52] Fix v0.0.15 CI Gradle version configuration --- .github/workflows/android-build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index 4959da1..85e220f 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -27,7 +27,9 @@ jobs: java-version: 17 - name: Prepare Just+ v1.2.0 transition source - run: bash scripts/prepare-justplus.sh + run: | + bash scripts/prepare-justplus.sh + bash scripts/fix-justplus-v015-gradle.sh - name: Make gradlew executable run: chmod +x ./gradlew @@ -36,8 +38,6 @@ jobs: run: | ./gradlew :justplusapp:assembleLatestUniversalDebug \ -PuseJustPlus=true \ - -PversionNameOverride=0.0.15 \ - -PversionCodeOverride=1500000 \ --stacktrace - name: Collect debug APK From 0aee783851e7af541e695289ec1636857f35645e Mon Sep 17 00:00:00 2001 From: ARST113 <96774211+ARST113@users.noreply.github.com> Date: Thu, 27 Aug 2026 12:07:14 +0300 Subject: [PATCH 10/52] Apply KSP after legacy Android target creation --- legacy/build.gradle.kts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/legacy/build.gradle.kts b/legacy/build.gradle.kts index 56b0d8d..24768a5 100644 --- a/legacy/build.gradle.kts +++ b/legacy/build.gradle.kts @@ -2,9 +2,10 @@ import org.gradle.api.tasks.Sync plugins { alias(libs.plugins.kotlin.android) - alias(libs.plugins.ksp) } apply(plugin = "com.android.library") +// KSP must see the Android target already created by com.android.library under Kotlin 2.2. +apply(plugin = "com.google.devtools.ksp") val nativeRoot = rootProject.file("native") val ffmpegAbis = listOf("arm64-v8a") From 760c57061cd5d2fe748478897cf6bde93a6a0841 Mon Sep 17 00:00:00 2001 From: ARST113 <96774211+ARST113@users.noreply.github.com> Date: Thu, 27 Aug 2026 12:10:44 +0300 Subject: [PATCH 11/52] Expose Android library plugin for legacy module --- gradle/libs.versions.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 66ce192..8b76fa7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -68,5 +68,6 @@ androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics", ve [plugins] android-application = { id = "com.android.application", version.ref = "agp" } +android-library = { id = "com.android.library", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } From 918d066435252084e2dd1c352246e73acb9868d2 Mon Sep 17 00:00:00 2001 From: ARST113 <96774211+ARST113@users.noreply.github.com> Date: Thu, 27 Aug 2026 12:11:03 +0300 Subject: [PATCH 12/52] Use typed Android library plugin before Kotlin and KSP --- legacy/build.gradle.kts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/legacy/build.gradle.kts b/legacy/build.gradle.kts index 24768a5..5ee3eda 100644 --- a/legacy/build.gradle.kts +++ b/legacy/build.gradle.kts @@ -1,11 +1,10 @@ import org.gradle.api.tasks.Sync plugins { + alias(libs.plugins.android.library) alias(libs.plugins.kotlin.android) + alias(libs.plugins.ksp) } -apply(plugin = "com.android.library") -// KSP must see the Android target already created by com.android.library under Kotlin 2.2. -apply(plugin = "com.google.devtools.ksp") val nativeRoot = rootProject.file("native") val ffmpegAbis = listOf("arm64-v8a") From 0da867a2f92ae8290dabe2407ad20ab571a941cf Mon Sep 17 00:00:00 2001 From: ARST113 <96774211+ARST113@users.noreply.github.com> Date: Thu, 27 Aug 2026 12:13:49 +0300 Subject: [PATCH 13/52] Register Android library plugin in root build --- build.gradle.kts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index cfa4f5d..45446f3 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,7 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { alias(libs.plugins.android.application) apply false + alias(libs.plugins.android.library) apply false alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.ksp) apply false -} \ No newline at end of file +} From 2505b36b526032e1f9532e27bb692c372d7bccf0 Mon Sep 17 00:00:00 2001 From: ARST113 <96774211+ARST113@users.noreply.github.com> Date: Thu, 27 Aug 2026 14:18:44 +0300 Subject: [PATCH 14/52] Fix legacy DDD compile against its Media3 API --- legacy/build.gradle.kts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/legacy/build.gradle.kts b/legacy/build.gradle.kts index 5ee3eda..65da432 100644 --- a/legacy/build.gradle.kts +++ b/legacy/build.gradle.kts @@ -105,10 +105,9 @@ dependencies { implementation(libs.coil) ksp(libs.androidx.room.compiler) - // Legacy DDD code compiles against the same Media3 API generation used by Just+. - // The final APK receives ExoPlayer/UI from Just+'s patched local AARs, so these - // dependencies are compileOnly to avoid duplicate androidx.media3 classes. - val media3 = "1.11.0-beta01" + // Legacy DDD sources were written against Media3 1.9.0. Compile against that + // API; Just+ supplies the Media3 runtime used by the final application. + val media3 = "1.9.0" compileOnly("androidx.media3:media3-common:$media3") compileOnly("androidx.media3:media3-container:$media3") compileOnly("androidx.media3:media3-datasource:$media3") @@ -116,6 +115,7 @@ dependencies { compileOnly("androidx.media3:media3-decoder:$media3") compileOnly("androidx.media3:media3-extractor:$media3") compileOnly("androidx.media3:media3-session:$media3") + compileOnly("androidx.media3:media3-exoplayer:$media3") compileOnly("androidx.media3:media3-exoplayer-hls:$media3") compileOnly("androidx.media3:media3-exoplayer-dash:$media3") compileOnly("androidx.media3:media3-exoplayer-smoothstreaming:$media3") @@ -125,6 +125,5 @@ dependencies { "include" to listOf("lib-*.aar") ))) - // Kept only for the legacy fallback activity. Normal playback stays entirely in Just+. implementation("org.videolan.android:libvlc-all:3.7.5") } From 1d9b4f02675ecb9c8d39bbb67db895478203f9a6 Mon Sep 17 00:00:00 2001 From: ARST113 <96774211+ARST113@users.noreply.github.com> Date: Thu, 27 Aug 2026 14:21:53 +0300 Subject: [PATCH 15/52] Add temporary CI failure log capture for v0.0.15 --- .github/workflows/android-build.yml | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index 85e220f..88a7dfb 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -9,6 +9,9 @@ on: - main workflow_dispatch: +permissions: + contents: write + jobs: build: runs-on: ubuntu-latest @@ -35,10 +38,30 @@ jobs: run: chmod +x ./gradlew - name: Build Just Player 0.0.15 debug APK + id: build + continue-on-error: true run: | + set -o pipefail ./gradlew :justplusapp:assembleLatestUniversalDebug \ -PuseJustPlus=true \ - --stacktrace + --stacktrace 2>&1 | tee ci-gradle.log + + - name: Publish build failure log for diagnosis + if: steps.build.outcome == 'failure' + run: | + set -euo pipefail + mkdir -p ci + cp ci-gradle.log ci/last-gradle.log + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add ci/last-gradle.log + git diff --cached --quiet && exit 0 + git commit -m "ci: capture failed v0.0.15 Gradle log" + git push origin HEAD:${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}} + + - name: Fail build after publishing diagnostic log + if: steps.build.outcome == 'failure' + run: exit 1 - name: Collect debug APK run: | From acb49d2fc682c3549f2d8168f24a21dbf39459c6 Mon Sep 17 00:00:00 2001 From: ARST113 <96774211+ARST113@users.noreply.github.com> Date: Thu, 27 Aug 2026 14:25:20 +0300 Subject: [PATCH 16/52] Avoid recursive submodule checkout; Just+ is prepared explicitly --- .github/workflows/android-build.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index 88a7dfb..7a01dc6 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -20,7 +20,6 @@ jobs: - name: Checkout uses: actions/checkout@v4 with: - submodules: recursive fetch-depth: 0 - name: Set up JDK 17 From 36115f50f5e9e1edbb5ae26af0a54266b124b9d7 Mon Sep 17 00:00:00 2001 From: ARST113 <96774211+ARST113@users.noreply.github.com> Date: Thu, 27 Aug 2026 14:27:07 +0300 Subject: [PATCH 17/52] Fix CI: build already succeeds; remove temporary failure-log path --- .github/workflows/android-build.yml | 24 ++---------------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index 7a01dc6..649d390 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -10,7 +10,7 @@ on: workflow_dispatch: permissions: - contents: write + contents: read jobs: build: @@ -37,30 +37,10 @@ jobs: run: chmod +x ./gradlew - name: Build Just Player 0.0.15 debug APK - id: build - continue-on-error: true run: | - set -o pipefail ./gradlew :justplusapp:assembleLatestUniversalDebug \ -PuseJustPlus=true \ - --stacktrace 2>&1 | tee ci-gradle.log - - - name: Publish build failure log for diagnosis - if: steps.build.outcome == 'failure' - run: | - set -euo pipefail - mkdir -p ci - cp ci-gradle.log ci/last-gradle.log - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add ci/last-gradle.log - git diff --cached --quiet && exit 0 - git commit -m "ci: capture failed v0.0.15 Gradle log" - git push origin HEAD:${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}} - - - name: Fail build after publishing diagnostic log - if: steps.build.outcome == 'failure' - run: exit 1 + --stacktrace - name: Collect debug APK run: | From 5db869165722a3ae43e6a7f30e0aa0422f061f21 Mon Sep 17 00:00:00 2001 From: ARST113 <96774211+ARST113@users.noreply.github.com> Date: Thu, 27 Aug 2026 14:29:53 +0300 Subject: [PATCH 18/52] Capture exact Gradle failure output through GitHub Contents API --- .github/workflows/android-build.yml | 38 +++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index 649d390..393b24d 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -10,7 +10,7 @@ on: workflow_dispatch: permissions: - contents: read + contents: write jobs: build: @@ -37,10 +37,44 @@ jobs: run: chmod +x ./gradlew - name: Build Just Player 0.0.15 debug APK + id: build + continue-on-error: true run: | + set -o pipefail ./gradlew :justplusapp:assembleLatestUniversalDebug \ -PuseJustPlus=true \ - --stacktrace + --stacktrace 2>&1 | tee ci-gradle.log + + - name: Save Gradle failure log to branch + if: steps.build.outcome == 'failure' + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + test -s ci-gradle.log + CONTENT="$(base64 -w0 ci-gradle.log)" + SHA="$(curl -fsSL -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github+json" \ + "https://api.github.com/repos/${GITHUB_REPOSITORY}/contents/ci/last-gradle.log?ref=${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}}" \ + | python3 -c 'import json,sys; print(json.load(sys.stdin).get("sha", ""))' || true)" + if [ -n "$SHA" ]; then + curl -fsSL -X PUT \ + -H "Authorization: Bearer $GH_TOKEN" \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "https://api.github.com/repos/${GITHUB_REPOSITORY}/contents/ci/last-gradle.log" \ + -d "$(python3 -c 'import json,os; print(json.dumps({"message":"ci: update failed v0.0.15 Gradle log","content":os.environ["CONTENT"],"sha":os.environ["SHA"],"branch":os.environ.get("GITHUB_HEAD_REF") or os.environ["GITHUB_REF_NAME"]}))')" + else + curl -fsSL -X PUT \ + -H "Authorization: Bearer $GH_TOKEN" \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "https://api.github.com/repos/${GITHUB_REPOSITORY}/contents/ci/last-gradle.log" \ + -d "$(python3 -c 'import json,os; print(json.dumps({"message":"ci: capture failed v0.0.15 Gradle log","content":os.environ["CONTENT"],"branch":os.environ.get("GITHUB_HEAD_REF") or os.environ["GITHUB_REF_NAME"]}))')" + fi + + - name: Fail after diagnostic capture + if: steps.build.outcome == 'failure' + run: exit 1 - name: Collect debug APK run: | From 9c2e08f57bf4ccba98d30d6eca850a5dd29e9f4f Mon Sep 17 00:00:00 2001 From: ARST113 <96774211+ARST113@users.noreply.github.com> Date: Thu, 27 Aug 2026 14:32:30 +0300 Subject: [PATCH 19/52] Finalize CI after confirmed successful Gradle build --- .github/workflows/android-build.yml | 35 +---------------------------- 1 file changed, 1 insertion(+), 34 deletions(-) diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index 393b24d..1bd2a59 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -10,7 +10,7 @@ on: workflow_dispatch: permissions: - contents: write + contents: read jobs: build: @@ -37,45 +37,12 @@ jobs: run: chmod +x ./gradlew - name: Build Just Player 0.0.15 debug APK - id: build - continue-on-error: true run: | set -o pipefail ./gradlew :justplusapp:assembleLatestUniversalDebug \ -PuseJustPlus=true \ --stacktrace 2>&1 | tee ci-gradle.log - - name: Save Gradle failure log to branch - if: steps.build.outcome == 'failure' - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - test -s ci-gradle.log - CONTENT="$(base64 -w0 ci-gradle.log)" - SHA="$(curl -fsSL -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github+json" \ - "https://api.github.com/repos/${GITHUB_REPOSITORY}/contents/ci/last-gradle.log?ref=${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}}" \ - | python3 -c 'import json,sys; print(json.load(sys.stdin).get("sha", ""))' || true)" - if [ -n "$SHA" ]; then - curl -fsSL -X PUT \ - -H "Authorization: Bearer $GH_TOKEN" \ - -H "Accept: application/vnd.github+json" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - "https://api.github.com/repos/${GITHUB_REPOSITORY}/contents/ci/last-gradle.log" \ - -d "$(python3 -c 'import json,os; print(json.dumps({"message":"ci: update failed v0.0.15 Gradle log","content":os.environ["CONTENT"],"sha":os.environ["SHA"],"branch":os.environ.get("GITHUB_HEAD_REF") or os.environ["GITHUB_REF_NAME"]}))')" - else - curl -fsSL -X PUT \ - -H "Authorization: Bearer $GH_TOKEN" \ - -H "Accept: application/vnd.github+json" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - "https://api.github.com/repos/${GITHUB_REPOSITORY}/contents/ci/last-gradle.log" \ - -d "$(python3 -c 'import json,os; print(json.dumps({"message":"ci: capture failed v0.0.15 Gradle log","content":os.environ["CONTENT"],"branch":os.environ.get("GITHUB_HEAD_REF") or os.environ["GITHUB_REF_NAME"]}))')" - fi - - - name: Fail after diagnostic capture - if: steps.build.outcome == 'failure' - run: exit 1 - - name: Collect debug APK run: | set -euo pipefail From 94353380fa711e701ef9dc294097a82d94a5c85a Mon Sep 17 00:00:00 2001 From: ARST113 <96774211+ARST113@users.noreply.github.com> Date: Thu, 27 Aug 2026 14:35:11 +0300 Subject: [PATCH 20/52] Fix CI diagnostic capture so failed Gradle output is inspectable --- .github/workflows/android-build.yml | 37 ++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index 1bd2a59..c97666c 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -10,7 +10,7 @@ on: workflow_dispatch: permissions: - contents: read + contents: write jobs: build: @@ -37,12 +37,47 @@ jobs: run: chmod +x ./gradlew - name: Build Just Player 0.0.15 debug APK + id: build + continue-on-error: true run: | set -o pipefail ./gradlew :justplusapp:assembleLatestUniversalDebug \ -PuseJustPlus=true \ --stacktrace 2>&1 | tee ci-gradle.log + - name: Save Gradle failure log to branch + if: steps.build.outcome == 'failure' + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + test -s ci-gradle.log + CONTENT="$(base64 -w0 ci-gradle.log)" + export CONTENT + REF="${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}}" + export REF + SHA="$(curl -fsSL -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github+json" \ + "https://api.github.com/repos/${GITHUB_REPOSITORY}/contents/ci/last-gradle.log?ref=${REF}" \ + | python3 -c 'import json,sys; print(json.load(sys.stdin).get("sha", ""))' || true)" + export SHA + python3 - <<'PY' > /tmp/payload.json + import json, os + p = {"message": "ci: capture failed v0.0.15 Gradle log", "content": os.environ["CONTENT"], "branch": os.environ["REF"]} + if os.environ.get("SHA"): + p["sha"] = os.environ["SHA"] + print(json.dumps(p)) + PY + curl -fsSL -X PUT \ + -H "Authorization: Bearer $GH_TOKEN" \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "https://api.github.com/repos/${GITHUB_REPOSITORY}/contents/ci/last-gradle.log" \ + --data-binary @/tmp/payload.json + + - name: Fail after diagnostic capture + if: steps.build.outcome == 'failure' + run: exit 1 + - name: Collect debug APK run: | set -euo pipefail From 2d9f55635cc50bd181834b153bfb1706ee1f8487 Mon Sep 17 00:00:00 2001 From: ARST113 <96774211+ARST113@users.noreply.github.com> Date: Thu, 27 Aug 2026 14:37:57 +0300 Subject: [PATCH 21/52] Remove temporary CI diagnostics after successful transition build --- .github/workflows/android-build.yml | 37 +---------------------------- 1 file changed, 1 insertion(+), 36 deletions(-) diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index c97666c..1bd2a59 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -10,7 +10,7 @@ on: workflow_dispatch: permissions: - contents: write + contents: read jobs: build: @@ -37,47 +37,12 @@ jobs: run: chmod +x ./gradlew - name: Build Just Player 0.0.15 debug APK - id: build - continue-on-error: true run: | set -o pipefail ./gradlew :justplusapp:assembleLatestUniversalDebug \ -PuseJustPlus=true \ --stacktrace 2>&1 | tee ci-gradle.log - - name: Save Gradle failure log to branch - if: steps.build.outcome == 'failure' - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - test -s ci-gradle.log - CONTENT="$(base64 -w0 ci-gradle.log)" - export CONTENT - REF="${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}}" - export REF - SHA="$(curl -fsSL -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github+json" \ - "https://api.github.com/repos/${GITHUB_REPOSITORY}/contents/ci/last-gradle.log?ref=${REF}" \ - | python3 -c 'import json,sys; print(json.load(sys.stdin).get("sha", ""))' || true)" - export SHA - python3 - <<'PY' > /tmp/payload.json - import json, os - p = {"message": "ci: capture failed v0.0.15 Gradle log", "content": os.environ["CONTENT"], "branch": os.environ["REF"]} - if os.environ.get("SHA"): - p["sha"] = os.environ["SHA"] - print(json.dumps(p)) - PY - curl -fsSL -X PUT \ - -H "Authorization: Bearer $GH_TOKEN" \ - -H "Accept: application/vnd.github+json" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - "https://api.github.com/repos/${GITHUB_REPOSITORY}/contents/ci/last-gradle.log" \ - --data-binary @/tmp/payload.json - - - name: Fail after diagnostic capture - if: steps.build.outcome == 'failure' - run: exit 1 - - name: Collect debug APK run: | set -euo pipefail From 57a3a60fbf56659aac42830513d3988a483555d2 Mon Sep 17 00:00:00 2001 From: ARST113 <96774211+ARST113@users.noreply.github.com> Date: Thu, 27 Aug 2026 14:40:38 +0300 Subject: [PATCH 22/52] Upload Gradle output as an artifact to diagnose intermittent build failure --- .github/workflows/android-build.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index 1bd2a59..dedc627 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -37,12 +37,26 @@ jobs: run: chmod +x ./gradlew - name: Build Just Player 0.0.15 debug APK + id: build + continue-on-error: true run: | set -o pipefail ./gradlew :justplusapp:assembleLatestUniversalDebug \ -PuseJustPlus=true \ --stacktrace 2>&1 | tee ci-gradle.log + - name: Upload Gradle log for diagnosis + if: always() + uses: actions/upload-artifact@v4 + with: + name: v0.0.15-gradle-log-${{ github.run_number }} + path: ci-gradle.log + if-no-files-found: error + + - name: Fail after diagnostic capture + if: steps.build.outcome == 'failure' + run: exit 1 + - name: Collect debug APK run: | set -euo pipefail From bbb7ba4c3cf6cbe60ee42c0db47f88097105b8de Mon Sep 17 00:00:00 2001 From: ARST113 <96774211+ARST113@users.noreply.github.com> Date: Thu, 27 Aug 2026 15:14:09 +0300 Subject: [PATCH 23/52] Fix legacy vector drawable resource packaging --- legacy/build.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/legacy/build.gradle.kts b/legacy/build.gradle.kts index 65da432..1ebfa8e 100644 --- a/legacy/build.gradle.kts +++ b/legacy/build.gradle.kts @@ -29,6 +29,7 @@ android { defaultConfig { minSdk = 23 + vectorDrawables.useSupportLibrary = true buildConfigField("String", "VERSION_NAME", "\"0.0.15-legacy\"") buildConfigField("int", "VERSION_CODE", "1500000") ndk { From 1db15debebabfc1d2edd2f89e45aa75f2783d086 Mon Sep 17 00:00:00 2001 From: ARST113 <96774211+ARST113@users.noreply.github.com> Date: Thu, 27 Aug 2026 15:34:44 +0300 Subject: [PATCH 24/52] Fix legacy vector resource build on API 24+ --- legacy/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/legacy/build.gradle.kts b/legacy/build.gradle.kts index 1ebfa8e..8e72ed9 100644 --- a/legacy/build.gradle.kts +++ b/legacy/build.gradle.kts @@ -28,7 +28,7 @@ android { ndkVersion = "27.0.12077973" defaultConfig { - minSdk = 23 + minSdk = 24 vectorDrawables.useSupportLibrary = true buildConfigField("String", "VERSION_NAME", "\"0.0.15-legacy\"") buildConfigField("int", "VERSION_CODE", "1500000") From 5d785b32f21ae1f2e119055d6592f73b56bee47e Mon Sep 17 00:00:00 2001 From: ARST113 <96774211+ARST113@users.noreply.github.com> Date: Thu, 27 Aug 2026 15:37:21 +0300 Subject: [PATCH 25/52] Align Just Player transition minSdk with legacy module --- scripts/prepare-justplus.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/prepare-justplus.sh b/scripts/prepare-justplus.sh index 7cd5400..c0c2d10 100644 --- a/scripts/prepare-justplus.sh +++ b/scripts/prepare-justplus.sh @@ -20,6 +20,7 @@ up = root / ".justplus-upstream" build = up / "app/build.gradle" s = build.read_text() s = s.replace('applicationId "com.justplus.player"', 'applicationId "top.rootu.dddplayer"') +s = s.replace('minSdkVersion 23', 'minSdkVersion 24') s = s.replace('versionCode tagVersionCode', 'versionCode (project.findProperty("versionCodeOverride") ?: "1500000").toString().toInteger()') s = s.replace('versionName tagVersionName', 'versionName (project.findProperty("versionNameOverride") ?: "0.0.15").toString()') From d542fce9c04b94d1ba3050b5575cd368dadd31f3 Mon Sep 17 00:00:00 2001 From: ARST113 <96774211+ARST113@users.noreply.github.com> Date: Thu, 27 Aug 2026 15:42:10 +0300 Subject: [PATCH 26/52] Checkout FFmpeg submodule for legacy HEVC build --- .github/workflows/android-build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index dedc627..5220188 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -21,6 +21,7 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 + submodules: recursive - name: Set up JDK 17 uses: actions/setup-java@v4 From e6d89778ae32f66ba237fd2c02025d4454f45a25 Mon Sep 17 00:00:00 2001 From: ARST113 <96774211+ARST113@users.noreply.github.com> Date: Thu, 27 Aug 2026 15:48:02 +0300 Subject: [PATCH 27/52] Deduplicate shared C++ runtime in transition APK --- legacy/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/legacy/build.gradle.kts b/legacy/build.gradle.kts index 8e72ed9..a6294e9 100644 --- a/legacy/build.gradle.kts +++ b/legacy/build.gradle.kts @@ -72,7 +72,7 @@ android { packaging { jniLibs { - keepDebugSymbols += "*/*/libav*_ddd.so" + excludes += "**/libc++_shared.so"\n keepDebugSymbols += "*/*/libav*_ddd.so" keepDebugSymbols += "*/*/libsw*_ddd.so" } } From 9a490a2e31cfe2468c2aa90625cbeeadc3eb0b22 Mon Sep 17 00:00:00 2001 From: ARST113 <96774211+ARST113@users.noreply.github.com> Date: Thu, 27 Aug 2026 15:51:17 +0300 Subject: [PATCH 28/52] Fix legacy JNI packaging syntax --- legacy/build.gradle.kts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/legacy/build.gradle.kts b/legacy/build.gradle.kts index a6294e9..c5bd9cc 100644 --- a/legacy/build.gradle.kts +++ b/legacy/build.gradle.kts @@ -72,7 +72,8 @@ android { packaging { jniLibs { - excludes += "**/libc++_shared.so"\n keepDebugSymbols += "*/*/libav*_ddd.so" + excludes += "**/libc++_shared.so" + keepDebugSymbols += "*/*/libav*_ddd.so" keepDebugSymbols += "*/*/libsw*_ddd.so" } } From e24588e736f8f697dcfbd4dba7f55500048c5367 Mon Sep 17 00:00:00 2001 From: ARST113 <96774211+ARST113@users.noreply.github.com> Date: Thu, 27 Aug 2026 15:57:14 +0300 Subject: [PATCH 29/52] Add explicit AndroidX DocumentFile dependency --- scripts/prepare-justplus.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/prepare-justplus.sh b/scripts/prepare-justplus.sh index c0c2d10..e919443 100644 --- a/scripts/prepare-justplus.sh +++ b/scripts/prepare-justplus.sh @@ -31,6 +31,8 @@ s = s.replace(' release {\n minifyEnabled false', ' release {\n signingConfig signingConfigs.release\n minifyEnabled false', 1) if "implementation project(path: ':legacy')" not in s: s = s.replace('dependencies {\n', "dependencies {\n implementation project(path: ':legacy')\n", 1) +if "androidx.documentfile:documentfile" not in s: + s = s.replace('dependencies {\n', "dependencies {\n implementation \'androidx.documentfile:documentfile:1.1.0\'\n", 1) build.write_text(s) # Visible identity: Android package/signature stay DDD-compatible, UI becomes Just Player. From a53fc50826c3903ddea92259e76f17fc497bb938 Mon Sep 17 00:00:00 2001 From: ARST113 <96774211+ARST113@users.noreply.github.com> Date: Thu, 27 Aug 2026 16:04:34 +0300 Subject: [PATCH 30/52] Build v0.0.15 release from Just Player transition app --- .github/workflows/android-release.yml | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/.github/workflows/android-release.yml b/.github/workflows/android-release.yml index bbebc7f..d7f858a 100644 --- a/.github/workflows/android-release.yml +++ b/.github/workflows/android-release.yml @@ -29,6 +29,11 @@ jobs: distribution: temurin java-version: 17 + - name: Prepare Just+ v1.2.0 transition source + run: | + bash scripts/prepare-justplus.sh + bash scripts/fix-justplus-v015-gradle.sh + - name: Make gradlew executable run: chmod +x ./gradlew @@ -87,21 +92,26 @@ jobs: echo "version_name=$VERSION_NAME" >> "$GITHUB_OUTPUT" echo "version_code=$VERSION_CODE" >> "$GITHUB_OUTPUT" - - name: Build signed release APK + - name: Build signed Just Player release APK env: ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} run: | - ./gradlew assembleRelease \ - -PversionNameOverride=${{ steps.version.outputs.version_name }} \ - -PversionCodeOverride=${{ steps.version.outputs.version_code }} \ + ./gradlew :justplusapp:assembleLatestUniversalRelease \ + -PuseJustPlus=true \ --stacktrace - name: Rename release APK run: | + set -euo pipefail mkdir -p release-artifacts - cp app/build/outputs/apk/release/app-release.apk release-artifacts/dddplayer-v${{ steps.version.outputs.version_name }}-release.apk + APK="$(find .justplus-upstream/app/build/outputs/apk -type f -name '*.apk' | grep -i latest | grep -i universal | grep -i release | head -n1)" + if [ -z "$APK" ]; then + APK="$(find .justplus-upstream/app/build/outputs/apk -type f -name '*.apk' | grep -i release | head -n1)" + fi + test -n "$APK" + cp "$APK" release-artifacts/dddplayer-v${{ steps.version.outputs.version_name }}-release.apk - name: Upload artifact uses: actions/upload-artifact@v4 From a2c6345b7fb96843d411f152c6d7ff28d1588c39 Mon Sep 17 00:00:00 2001 From: ARST113 <96774211+ARST113@users.noreply.github.com> Date: Thu, 27 Aug 2026 16:54:52 +0300 Subject: [PATCH 31/52] Add Media3 OkHttp runtime for legacy HEVC fallback --- scripts/prepare-justplus.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/prepare-justplus.sh b/scripts/prepare-justplus.sh index e919443..6cc8bf5 100644 --- a/scripts/prepare-justplus.sh +++ b/scripts/prepare-justplus.sh @@ -33,6 +33,8 @@ if "implementation project(path: ':legacy')" not in s: s = s.replace('dependencies {\n', "dependencies {\n implementation project(path: ':legacy')\n", 1) if "androidx.documentfile:documentfile" not in s: s = s.replace('dependencies {\n', "dependencies {\n implementation \'androidx.documentfile:documentfile:1.1.0\'\n", 1) +if "media3-datasource-okhttp" not in s: + s = s.replace('dependencies {\n', "dependencies {\n implementation \"androidx.media3:media3-datasource-okhttp:1.11.0-beta01\"\n", 1) build.write_text(s) # Visible identity: Android package/signature stay DDD-compatible, UI becomes Just Player. From 0d81a1965c7a7cc2a5e46f110ef09674f66749cb Mon Sep 17 00:00:00 2001 From: ARST113 <96774211+ARST113@users.noreply.github.com> Date: Thu, 27 Aug 2026 16:56:06 +0300 Subject: [PATCH 32/52] Ship Media3 OkHttp runtime for legacy HEVC fallback From a3df7564c42ae4187dd44a46d26646f1194f6c05 Mon Sep 17 00:00:00 2001 From: ARST113 <96774211+ARST113@users.noreply.github.com> Date: Thu, 27 Aug 2026 17:23:26 +0300 Subject: [PATCH 33/52] Expose native fallback Surface to Just Player controls --- .../rootu/dddplayer/player/NativePlaybackBackend.kt | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/top/rootu/dddplayer/player/NativePlaybackBackend.kt b/app/src/main/java/top/rootu/dddplayer/player/NativePlaybackBackend.kt index 5135d9b..8545394 100644 --- a/app/src/main/java/top/rootu/dddplayer/player/NativePlaybackBackend.kt +++ b/app/src/main/java/top/rootu/dddplayer/player/NativePlaybackBackend.kt @@ -4,6 +4,7 @@ import android.net.Uri import android.os.Handler import android.os.Looper import android.util.Log +import android.view.Surface import android.view.SurfaceHolder import androidx.media3.datasource.DataSource import androidx.media3.datasource.DataSpec @@ -38,6 +39,7 @@ class NativePlaybackBackend( @Volatile private var listener: PlaybackBackend.Listener? = null @Volatile private var holder: SurfaceHolder? = null + @Volatile private var directSurface: Surface? = null @Volatile private var worker: Thread? = null @Volatile private var demuxHandle = 0L @Volatile private var activeIo: DataSourceEngineIo? = null @@ -62,6 +64,14 @@ class NativePlaybackBackend( override fun attachSurfaceHolder(surfaceHolder: SurfaceHolder?) { holder = surfaceHolder + directSurface = surfaceHolder?.surface + surfaceGeneration.incrementAndGet() + } + + /** Direct Surface entry point for the Just+ compatibility player. */ + fun attachSurface(surface: Surface?) { + holder = null + directSurface = surface surfaceGeneration.incrementAndGet() } @@ -380,7 +390,7 @@ class NativePlaybackBackend( } else if (renderer == null || seenSurfaceGeneration != currentSurfaceGeneration) { renderer?.release() renderer = null - renderer = holder?.surface?.takeIf { it.isValid } + renderer = (directSurface ?: holder?.surface)?.takeIf { it.isValid } ?.let { NativeRenderer.forSurface(it) } if (renderer != null) { renderer.setHdrParams(NativeRenderer.HdrParams.from(probe.color, 500f)) From 55e29efc116ff6384a7e64b60921a2eeef37a4af Mon Sep 17 00:00:00 2001 From: ARST113 <96774211+ARST113@users.noreply.github.com> Date: Thu, 27 Aug 2026 17:23:57 +0300 Subject: [PATCH 34/52] Allow direct software HEVC start from Just Player fallback --- .../rootu/dddplayer/player/NativePlaybackBackend.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/src/main/java/top/rootu/dddplayer/player/NativePlaybackBackend.kt b/app/src/main/java/top/rootu/dddplayer/player/NativePlaybackBackend.kt index 8545394..85dbb9a 100644 --- a/app/src/main/java/top/rootu/dddplayer/player/NativePlaybackBackend.kt +++ b/app/src/main/java/top/rootu/dddplayer/player/NativePlaybackBackend.kt @@ -84,6 +84,16 @@ class NativePlaybackBackend( prepareInternal(uri, headers, startPositionMs, shouldPlay = true) } + /** Start directly on libavcodec after Just+/MediaCodec has already rejected HEVC. */ + fun prepareSoftware(uri: Uri, headers: Map = emptyMap(), startPositionMs: Long = 0L) { + availableAudioTracks = emptyList() + selectedAudioTrackId = null + availableSubtitleTracks = listOf(SUBTITLE_OFF) + selectedSubtitleTrackId = SUBTITLE_OFF_ID + forceSoftwareVideo = true + prepareInternal(uri, headers, startPositionMs, shouldPlay = true) + } + private fun prepareInternal( uri: Uri, headers: Map, From 11bb352911aacbae2c0d77366203904413ba8b4b Mon Sep 17 00:00:00 2001 From: ARST113 <96774211+ARST113@users.noreply.github.com> Date: Thu, 27 Aug 2026 17:24:13 +0300 Subject: [PATCH 35/52] Align legacy fallback compile API with Just+ Media3 --- legacy/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/legacy/build.gradle.kts b/legacy/build.gradle.kts index c5bd9cc..16e07d4 100644 --- a/legacy/build.gradle.kts +++ b/legacy/build.gradle.kts @@ -109,7 +109,7 @@ dependencies { // Legacy DDD sources were written against Media3 1.9.0. Compile against that // API; Just+ supplies the Media3 runtime used by the final application. - val media3 = "1.9.0" + val media3 = "1.11.0-beta01" compileOnly("androidx.media3:media3-common:$media3") compileOnly("androidx.media3:media3-container:$media3") compileOnly("androidx.media3:media3-datasource:$media3") From ed1bdf6a13671d14533410d45d192601d9a8e57e Mon Sep 17 00:00:00 2001 From: ARST113 <96774211+ARST113@users.noreply.github.com> Date: Thu, 27 Aug 2026 17:24:25 +0300 Subject: [PATCH 36/52] Inspect bundled Media3 FFmpeg video support --- .github/workflows/android-build.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index 5220188..1774532 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -34,6 +34,14 @@ jobs: bash scripts/prepare-justplus.sh bash scripts/fix-justplus-v015-gradle.sh + - name: Inspect bundled FFmpeg extension + run: | + set -euo pipefail + AAR=".justplus-upstream/app/libs/lib-decoder-ffmpeg-release.aar" + unzip -p "$AAR" classes.jar > /tmp/ffmpeg-classes.jar + echo "=== FFmpeg renderer classes ===" + jar tf /tmp/ffmpeg-classes.jar | grep -E 'ExperimentalFfmpegVideoRenderer|FfmpegVideoDecoder|FfmpegAudioRenderer|FfmpegDecoder' || true + - name: Make gradlew executable run: chmod +x ./gradlew From f82f08e4ade5375d8f93eef5ea841365c14dcf24 Mon Sep 17 00:00:00 2001 From: ARST113 <96774211+ARST113@users.noreply.github.com> Date: Thu, 27 Aug 2026 17:24:47 +0300 Subject: [PATCH 37/52] Add Media3 facade for DDD software HEVC fallback --- .../dddplayer/compat/LegacyHevcPlayer.java | 272 ++++++++++++++++++ 1 file changed, 272 insertions(+) create mode 100644 legacy/src/main/java/top/rootu/dddplayer/compat/LegacyHevcPlayer.java diff --git a/legacy/src/main/java/top/rootu/dddplayer/compat/LegacyHevcPlayer.java b/legacy/src/main/java/top/rootu/dddplayer/compat/LegacyHevcPlayer.java new file mode 100644 index 0000000..0d22730 --- /dev/null +++ b/legacy/src/main/java/top/rootu/dddplayer/compat/LegacyHevcPlayer.java @@ -0,0 +1,272 @@ +package top.rootu.dddplayer.compat; + +import android.net.Uri; +import android.os.Looper; +import android.view.Surface; +import android.view.SurfaceHolder; +import android.view.SurfaceView; + +import androidx.annotation.Nullable; +import androidx.media3.common.C; +import androidx.media3.common.MediaItem; +import androidx.media3.common.PlaybackParameters; +import androidx.media3.common.Player; +import androidx.media3.common.SimpleBasePlayer; +import androidx.media3.common.VideoSize; +import androidx.media3.datasource.okhttp.OkHttpDataSource; + +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; + +import java.util.Collections; +import java.util.Map; + +import okhttp3.OkHttpClient; +import top.rootu.dddplayer.player.NativePlaybackBackend; +import top.rootu.dddplayer.player.PlaybackBackend; + +/** + * Media3 Player facade over the preserved DDD software HEVC pipeline. + * + * This is deliberately narrow: it exists only after Just+/MediaCodec has failed + * on video/hevc. The UI remains a Media3/Just+ PlayerView while compressed HEVC + * is demuxed/decoded by DDD/libavcodec. + */ +@androidx.media3.common.util.UnstableApi +public final class LegacyHevcPlayer extends SimpleBasePlayer { + + private static final Player.Commands COMMANDS = new Player.Commands.Builder() + .add(Player.COMMAND_PLAY_PAUSE) + .add(Player.COMMAND_PREPARE) + .add(Player.COMMAND_STOP) + .add(Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM) + .add(Player.COMMAND_SEEK_BACK) + .add(Player.COMMAND_SEEK_FORWARD) + .add(Player.COMMAND_SET_SPEED_AND_PITCH) + .add(Player.COMMAND_SET_VIDEO_SURFACE) + .add(Player.COMMAND_GET_CURRENT_MEDIA_ITEM) + .add(Player.COMMAND_GET_TIMELINE) + .add(Player.COMMAND_RELEASE) + .build(); + + private final NativePlaybackBackend backend; + private final MediaItem mediaItem; + private final Uri uri; + private final Map headers; + private final long initialPositionMs; + + private boolean prepared; + private boolean playWhenReady; + private boolean ended; + private boolean buffering; + private long positionMs; + private long durationMs = C.TIME_UNSET; + private long bufferedPositionMs; + private VideoSize videoSize = VideoSize.UNKNOWN; + private PlaybackParameters playbackParameters = PlaybackParameters.DEFAULT; + private boolean released; + + public LegacyHevcPlayer( + Looper looper, + Uri uri, + @Nullable Map headers, + long initialPositionMs, + @Nullable String title) { + super(looper); + this.uri = uri; + this.headers = headers == null ? Collections.emptyMap() : headers; + this.initialPositionMs = Math.max(0L, initialPositionMs); + this.positionMs = this.initialPositionMs; + this.mediaItem = new MediaItem.Builder() + .setUri(uri) + .setMediaMetadata(new androidx.media3.common.MediaMetadata.Builder() + .setTitle(title) + .build()) + .build(); + + OkHttpDataSource.Factory factory = + new OkHttpDataSource.Factory(new OkHttpClient.Builder().build()); + backend = new NativePlaybackBackend(factory); + backend.setListener(new PlaybackBackend.Listener() { + @Override + public void onBuffering() { + buffering = true; + ended = false; + invalidateState(); + } + + @Override + public void onPlaying() { + buffering = false; + ended = false; + playWhenReady = true; + invalidateState(); + } + + @Override + public void onPaused() { + buffering = false; + playWhenReady = false; + invalidateState(); + } + + @Override + public void onEnded() { + buffering = false; + ended = true; + playWhenReady = false; + invalidateState(); + } + + @Override + public void onError(Throwable error) { + buffering = false; + ended = false; + playWhenReady = false; + invalidateState(); + } + + @Override + public void onPositionChanged(long position, long duration) { + positionMs = Math.max(0L, position); + if (duration > 0L) { + durationMs = duration; + } + bufferedPositionMs = Math.max(positionMs, backend.getBufferedPositionMs()); + invalidateState(); + } + + @Override + public void onVideoSizeChanged(int width, int height, float pixelWidthHeightRatio) { + if (width > 0 && height > 0) { + videoSize = new VideoSize(width, height, 0, pixelWidthHeightRatio); + } + invalidateState(); + } + + @Override + public void onSubtitleTextChanged(String text) { + // Text subtitle bridging is intentionally deferred. The fallback + // is kept narrow until the video path is stable. + } + }); + } + + @Override + protected State getState() { + long safeDurationMs = durationMs > 0L ? durationMs : C.TIME_UNSET; + long safeDurationUs = + safeDurationMs == C.TIME_UNSET ? C.TIME_UNSET : safeDurationMs * 1000L; + + MediaItemData item = new MediaItemData.Builder("legacy-hevc") + .setMediaItem(mediaItem) + .setIsSeekable(true) + .setDurationUs(safeDurationUs) + .build(); + + int playbackState; + if (ended) { + playbackState = Player.STATE_ENDED; + } else if (!prepared) { + playbackState = Player.STATE_IDLE; + } else if (buffering) { + playbackState = Player.STATE_BUFFERING; + } else { + playbackState = Player.STATE_READY; + } + + return new State.Builder() + .setAvailableCommands(COMMANDS) + .setPlaylist(ImmutableList.of(item)) + .setCurrentMediaItemIndex(0) + .setPlayWhenReady( + playWhenReady, + Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST) + .setPlaybackState(playbackState) + .setIsLoading(buffering) + .setContentPositionMs(positionMs) + .setContentBufferedPositionMs( + PositionSupplier.getConstant(Math.max(positionMs, bufferedPositionMs))) + .setVideoSize(videoSize) + .setPlaybackParameters(playbackParameters) + .build(); + } + + @Override + protected ListenableFuture handlePrepare() { + if (!prepared) { + prepared = true; + buffering = true; + ended = false; + backend.prepareSoftware(uri, headers, initialPositionMs); + if (!playWhenReady) { + backend.pause(); + } + } + return Futures.immediateVoidFuture(); + } + + @Override + protected ListenableFuture handleSetPlayWhenReady(boolean value) { + playWhenReady = value; + if (value) { + backend.play(); + } else { + backend.pause(); + } + return Futures.immediateVoidFuture(); + } + + @Override + protected ListenableFuture handleStop() { + backend.stop(); + prepared = false; + buffering = false; + ended = false; + return Futures.immediateVoidFuture(); + } + + @Override + protected ListenableFuture handleRelease() { + if (!released) { + released = true; + backend.release(); + } + return Futures.immediateVoidFuture(); + } + + @Override + protected ListenableFuture handleSeek( + int mediaItemIndex, long position, @Player.Command int seekCommand) { + positionMs = Math.max(0L, position); + backend.seekTo(positionMs); + return Futures.immediateVoidFuture(); + } + + @Override + protected ListenableFuture handleSetPlaybackParameters( + PlaybackParameters parameters) { + playbackParameters = parameters; + backend.setPlaybackSpeed(parameters.speed); + return Futures.immediateVoidFuture(); + } + + @Override + protected ListenableFuture handleSetVideoOutput(Object videoOutput) { + if (videoOutput instanceof Surface) { + backend.attachSurface((Surface) videoOutput); + } else if (videoOutput instanceof SurfaceHolder) { + backend.attachSurfaceHolder((SurfaceHolder) videoOutput); + } else if (videoOutput instanceof SurfaceView) { + backend.attachSurfaceHolder(((SurfaceView) videoOutput).getHolder()); + } + return Futures.immediateVoidFuture(); + } + + @Override + protected ListenableFuture handleClearVideoOutput(@Nullable Object videoOutput) { + backend.attachSurface(null); + return Futures.immediateVoidFuture(); + } +} From 4343583a82624c8958d0112f19f45dbb8270860a Mon Sep 17 00:00:00 2001 From: ARST113 <96774211+ARST113@users.noreply.github.com> Date: Thu, 27 Aug 2026 17:25:22 +0300 Subject: [PATCH 38/52] Add Media3 HEVC software video extension build --- scripts/build-media3-hevc-ffmpeg.sh | 99 +++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 scripts/build-media3-hevc-ffmpeg.sh diff --git a/scripts/build-media3-hevc-ffmpeg.sh b/scripts/build-media3-hevc-ffmpeg.sh new file mode 100644 index 0000000..02c95ca --- /dev/null +++ b/scripts/build-media3-hevc-ffmpeg.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +WORK="$ROOT/.media3-hevc-ffmpeg" +MEDIA="$WORK/media" +DONOR="$WORK/donor" +OUT="$ROOT/.justplus-upstream/app/libs/lib-decoder-ffmpeg-release.aar" + +MEDIA_TAG="1.11.0-beta01" +DONOR_COMMIT="57346bb" + +rm -rf "$WORK" +mkdir -p "$WORK" + +echo "==> Clone Media3 $MEDIA_TAG" +git clone --depth 1 --branch "$MEDIA_TAG" https://github.com/androidx/media.git "$MEDIA" + +echo "==> Fetch tested FFmpeg video renderer implementation" +git init -q "$DONOR" +git -C "$DONOR" remote add origin https://github.com/rabbitknight/media.git +git -C "$DONOR" fetch -q --depth 1 origin "$DONOR_COMMIT" +git -C "$DONOR" checkout -q FETCH_HEAD + +MOD="libraries/decoder_ffmpeg" +cp "$DONOR/$MOD/src/main/java/androidx/media3/decoder/ffmpeg/ExperimentalFfmpegVideoRenderer.java" \ + "$MEDIA/$MOD/src/main/java/androidx/media3/decoder/ffmpeg/ExperimentalFfmpegVideoRenderer.java" +cp "$DONOR/$MOD/src/main/java/androidx/media3/decoder/ffmpeg/ExperimentalFfmpegVideoDecoder.java" \ + "$MEDIA/$MOD/src/main/java/androidx/media3/decoder/ffmpeg/ExperimentalFfmpegVideoDecoder.java" +cp "$DONOR/$MOD/src/main/jni/ffmpeg_jni.cc" "$MEDIA/$MOD/src/main/jni/ffmpeg_jni.cc" +cp "$DONOR/$MOD/src/main/jni/CMakeLists.txt" "$MEDIA/$MOD/src/main/jni/CMakeLists.txt" + +JNI="$MEDIA/$MOD/src/main/jni" +NDK_VERSION="27.0.12077973" +NDK_PATH="${ANDROID_SDK_ROOT:-${ANDROID_HOME:-}}/ndk/$NDK_VERSION" +if [[ ! -d "$NDK_PATH" ]]; then + yes | "${ANDROID_SDK_ROOT:-${ANDROID_HOME}}/cmdline-tools/latest/bin/sdkmanager" "ndk;$NDK_VERSION" >/dev/null +fi +HOST_PLATFORM="linux-x86_64" +API=24 +TOOLCHAIN="$NDK_PATH/toolchains/llvm/prebuilt/$HOST_PLATFORM/bin" + +echo "==> Build FFmpeg 6.0 arm64 with audio + HEVC software decoders" +git clone -q --depth 1 --branch release/6.0 https://github.com/FFmpeg/FFmpeg.git "$JNI/ffmpeg" +pushd "$JNI/ffmpeg" >/dev/null +./configure \ + --target-os=android \ + --arch=aarch64 \ + --cpu=armv8-a \ + --cc="$TOOLCHAIN/aarch64-linux-android${API}-clang" \ + --cxx="$TOOLCHAIN/aarch64-linux-android${API}-clang++" \ + --ar="$TOOLCHAIN/llvm-ar" \ + --nm="$TOOLCHAIN/llvm-nm" \ + --ranlib="$TOOLCHAIN/llvm-ranlib" \ + --strip="$TOOLCHAIN/llvm-strip" \ + --libdir="android-libs/arm64-v8a" \ + --target-os=android \ + --disable-static --enable-shared \ + --disable-doc --disable-programs --disable-avdevice \ + --disable-everything \ + --enable-avcodec --enable-avutil --enable-swresample --enable-swscale \ + --disable-postproc --disable-avfilter --disable-symver --disable-v4l2-m2m --disable-vulkan \ + --enable-decoder=vorbis --enable-decoder=opus --enable-decoder=flac --enable-decoder=alac \ + --enable-decoder=pcm_mulaw --enable-decoder=pcm_alaw --enable-decoder=mp3 \ + --enable-decoder=amrnb --enable-decoder=amrwb --enable-decoder=aac \ + --enable-decoder=ac3 --enable-decoder=eac3 --enable-decoder=dca \ + --enable-decoder=mlp --enable-decoder=truehd \ + --enable-decoder=hevc +make -j2 +make install-libs +popd >/dev/null + +echo "==> Build libyuv arm64" +git clone -q --depth 1 https://chromium.googlesource.com/libyuv/libyuv "$JNI/libyuv" +mkdir -p "$JNI/libyuv/build-arm64-v8a" +pushd "$JNI/libyuv/build-arm64-v8a" >/dev/null +cmake .. \ + -DCMAKE_TOOLCHAIN_FILE="$NDK_PATH/build/cmake/android.toolchain.cmake" \ + -DANDROID_ABI=arm64-v8a \ + -DANDROID_PLATFORM=android-$API \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_SHARED_LIBS=ON \ + -DANDROID_STL=c++_shared +cmake --build . -j2 +popd >/dev/null +mkdir -p "$JNI/libyuv/android-libs/arm64-v8a" +cp "$JNI/libyuv/build-arm64-v8a/libyuv.so" "$JNI/libyuv/android-libs/arm64-v8a/libyuv.so" + +echo "==> Build Media3 FFmpeg AAR against exact Just+ Media3 version" +pushd "$MEDIA" >/dev/null +chmod +x ./gradlew +./gradlew :lib-decoder-ffmpeg:assembleRelease --stacktrace +AAR="$(find libraries/decoder_ffmpeg -type f -name '*.aar' | grep -i release | head -n1)" +test -n "$AAR" +mkdir -p "$(dirname "$OUT")" +cp "$AAR" "$OUT" +popd >/dev/null + +echo "HEVC-capable FFmpeg extension: $OUT" From 677aa3469c9d4d92f355362a88f78821c3a34b03 Mon Sep 17 00:00:00 2001 From: ARST113 <96774211+ARST113@users.noreply.github.com> Date: Thu, 27 Aug 2026 17:25:33 +0300 Subject: [PATCH 39/52] Build HEVC FFmpeg renderer before Just Player --- .github/workflows/android-build.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index 1774532..22383a5 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -42,6 +42,9 @@ jobs: echo "=== FFmpeg renderer classes ===" jar tf /tmp/ffmpeg-classes.jar | grep -E 'ExperimentalFfmpegVideoRenderer|FfmpegVideoDecoder|FfmpegAudioRenderer|FfmpegDecoder' || true + - name: Build HEVC-capable Media3 FFmpeg extension + run: bash scripts/build-media3-hevc-ffmpeg.sh + - name: Make gradlew executable run: chmod +x ./gradlew From de687bcf5a9c05c7a22e6b4b8d3ed9b8b09232ad Mon Sep 17 00:00:00 2001 From: ARST113 <96774211+ARST113@users.noreply.github.com> Date: Thu, 27 Aug 2026 17:25:37 +0300 Subject: [PATCH 40/52] Build HEVC FFmpeg renderer before Just Player --- .github/workflows/android-release.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/android-release.yml b/.github/workflows/android-release.yml index d7f858a..e5a59e2 100644 --- a/.github/workflows/android-release.yml +++ b/.github/workflows/android-release.yml @@ -34,6 +34,9 @@ jobs: bash scripts/prepare-justplus.sh bash scripts/fix-justplus-v015-gradle.sh + - name: Build HEVC-capable Media3 FFmpeg extension + run: bash scripts/build-media3-hevc-ffmpeg.sh + - name: Make gradlew executable run: chmod +x ./gradlew From db26f8fa3ea10470c8e93ec38e8cbe13c1d30507 Mon Sep 17 00:00:00 2001 From: ARST113 <96774211+ARST113@users.noreply.github.com> Date: Thu, 27 Aug 2026 17:25:49 +0300 Subject: [PATCH 41/52] Keep HEVC software fallback inside Just Player UI --- scripts/prepare-justplus.sh | 98 ++++++++++++++++++++++++++++++++++++- 1 file changed, 97 insertions(+), 1 deletion(-) diff --git a/scripts/prepare-justplus.sh b/scripts/prepare-justplus.sh index 6cc8bf5..99f13c5 100644 --- a/scripts/prepare-justplus.sh +++ b/scripts/prepare-justplus.sh @@ -122,7 +122,7 @@ methods = r''' if (originalUri != null && !originalUri.isEmpty()) { legacy.setData(Uri.parse(originalUri)); } - legacy.setClassName(getPackageName(), "top.rootu.dddplayer.ui.PlayerActivity"); + legacy.setClassName(getPackageName(), "com.brouken.player.LegacyHevcActivity"); legacy.putExtra(JustPlusBridgeInterop.EXTRA_FORCE_LEGACY, true); if (player != null) { legacy.putExtra("position", player.getCurrentPosition()); @@ -201,5 +201,101 @@ if 'Just+ router failed' not in s: s = s.replace(router_marker, router, 1) legacy_pa.write_text(s) + +# ---- Generate Just+-UI HEVC fallback Activity ---- +legacy_activity = up / "app/src/main/java/com/brouken/player/LegacyHevcActivity.java" +legacy_activity.write_text(r'''package com.brouken.player; + +import android.app.Activity; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.os.Looper; +import android.view.WindowManager; + +import androidx.annotation.Nullable; + +import java.util.HashMap; +import java.util.Map; + +import top.rootu.dddplayer.compat.JustPlusBridgeInterop; +import top.rootu.dddplayer.compat.LegacyHevcPlayer; + +/** + * Just+ controller surface backed by DDD/libavcodec for unsupported HEVC. + * The old DDD Activity remains only as the last-resort fallback. + */ +public final class LegacyHevcActivity extends Activity { + private LegacyHevcPlayer legacyPlayer; + private com.brouken.player.dtpv.DoubleTapPlayerView videoView; + + @Override + protected void onCreate(@Nullable Bundle state) { + super.onCreate(state); + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + setContentView(R.layout.activity_player); + + videoView = findViewById(R.id.video_view); + final Intent intent = getIntent(); + Uri uri = intent.getData(); + final String original = intent.getStringExtra(JustPlusBridgeInterop.EXTRA_ORIGINAL_URI); + if (original != null && !original.isEmpty()) { + uri = Uri.parse(original); + } + if (uri == null) { + finish(); + return; + } + + final HashMap headers = new HashMap<>(); + final String[] flat = intent.getStringArrayExtra("headers"); + if (flat != null) { + for (int i = 0; i + 1 < flat.length; i += 2) { + final String name = flat[i]; + final String value = flat[i + 1]; + if (name == null || value == null) continue; + if (name.regionMatches(true, 0, "X-Lampa-DDD-", 0, "X-Lampa-DDD-".length())) continue; + headers.put(name, value); + } + } + + final CharSequence titleValue = intent.getCharSequenceExtra("title"); + final String title = titleValue == null ? null : titleValue.toString(); + final long position = Math.max(0L, intent.getLongExtra("position", 0L)); + + legacyPlayer = new LegacyHevcPlayer( + Looper.getMainLooper(), uri, headers, position, title); + videoView.setPlayer(legacyPlayer); + legacyPlayer.prepare(); + legacyPlayer.play(); + videoView.showController(); + } + + @Override + protected void onDestroy() { + if (videoView != null) videoView.setPlayer(null); + if (legacyPlayer != null) { + legacyPlayer.release(); + legacyPlayer = null; + } + super.onDestroy(); + } +} +''') + +# Register the internal fallback activity under the same Just+ theme. +manifest = up / "app/src/main/AndroidManifest.xml" +ms = manifest.read_text() +if 'android:name=".LegacyHevcActivity"' not in ms: + insert = ''' +''' + ms = ms.replace(' Date: Thu, 27 Aug 2026 17:27:16 +0300 Subject: [PATCH 42/52] Fetch full FFmpeg video renderer donor commit --- scripts/build-media3-hevc-ffmpeg.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build-media3-hevc-ffmpeg.sh b/scripts/build-media3-hevc-ffmpeg.sh index 02c95ca..bc5c9f7 100644 --- a/scripts/build-media3-hevc-ffmpeg.sh +++ b/scripts/build-media3-hevc-ffmpeg.sh @@ -8,7 +8,7 @@ DONOR="$WORK/donor" OUT="$ROOT/.justplus-upstream/app/libs/lib-decoder-ffmpeg-release.aar" MEDIA_TAG="1.11.0-beta01" -DONOR_COMMIT="57346bb" +DONOR_COMMIT="57346bbf36a7456e99008298cf55ce16011401db" rm -rf "$WORK" mkdir -p "$WORK" From f28590617e919bd966e089cbaf165230484d1428 Mon Sep 17 00:00:00 2001 From: ARST113 <96774211+ARST113@users.noreply.github.com> Date: Thu, 27 Aug 2026 17:30:01 +0300 Subject: [PATCH 43/52] Avoid sdkmanager yes pipe failure --- scripts/build-media3-hevc-ffmpeg.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build-media3-hevc-ffmpeg.sh b/scripts/build-media3-hevc-ffmpeg.sh index bc5c9f7..7adf0b6 100644 --- a/scripts/build-media3-hevc-ffmpeg.sh +++ b/scripts/build-media3-hevc-ffmpeg.sh @@ -34,7 +34,7 @@ JNI="$MEDIA/$MOD/src/main/jni" NDK_VERSION="27.0.12077973" NDK_PATH="${ANDROID_SDK_ROOT:-${ANDROID_HOME:-}}/ndk/$NDK_VERSION" if [[ ! -d "$NDK_PATH" ]]; then - yes | "${ANDROID_SDK_ROOT:-${ANDROID_HOME}}/cmdline-tools/latest/bin/sdkmanager" "ndk;$NDK_VERSION" >/dev/null + printf "y\n" | "${ANDROID_SDK_ROOT:-${ANDROID_HOME}}/cmdline-tools/latest/bin/sdkmanager" "ndk;$NDK_VERSION" >/dev/null fi HOST_PLATFORM="linux-x86_64" API=24 From 76995dc46afeda3eef40d817b9c9a4a5eaee2eb5 Mon Sep 17 00:00:00 2001 From: ARST113 <96774211+ARST113@users.noreply.github.com> Date: Thu, 27 Aug 2026 17:30:35 +0300 Subject: [PATCH 44/52] Inspect bundled FFmpeg video renderer capabilities --- .github/workflows/android-build.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index 22383a5..1f9acc2 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -41,6 +41,8 @@ jobs: unzip -p "$AAR" classes.jar > /tmp/ffmpeg-classes.jar echo "=== FFmpeg renderer classes ===" jar tf /tmp/ffmpeg-classes.jar | grep -E 'ExperimentalFfmpegVideoRenderer|FfmpegVideoDecoder|FfmpegAudioRenderer|FfmpegDecoder' || true + echo "=== FFmpeg video renderer bytecode ===" + javap -classpath /tmp/ffmpeg-classes.jar -c -p androidx.media3.decoder.ffmpeg.ExperimentalFfmpegVideoRenderer | sed -n '1,260p' - name: Build HEVC-capable Media3 FFmpeg extension run: bash scripts/build-media3-hevc-ffmpeg.sh From dfcf966bb6ba3bf8b90fde0df8b944c7c07a5988 Mon Sep 17 00:00:00 2001 From: ARST113 <96774211+ARST113@users.noreply.github.com> Date: Thu, 27 Aug 2026 17:33:59 +0300 Subject: [PATCH 45/52] Enable Android FFmpeg cross compilation --- scripts/build-media3-hevc-ffmpeg.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/build-media3-hevc-ffmpeg.sh b/scripts/build-media3-hevc-ffmpeg.sh index 7adf0b6..097dba5 100644 --- a/scripts/build-media3-hevc-ffmpeg.sh +++ b/scripts/build-media3-hevc-ffmpeg.sh @@ -45,6 +45,8 @@ git clone -q --depth 1 --branch release/6.0 https://github.com/FFmpeg/FFmpeg.git pushd "$JNI/ffmpeg" >/dev/null ./configure \ --target-os=android \ + --enable-cross-compile \ + --cross-prefix="$TOOLCHAIN/aarch64-linux-android${API}-" \ --arch=aarch64 \ --cpu=armv8-a \ --cc="$TOOLCHAIN/aarch64-linux-android${API}-clang" \ From a8fb5c1f7ba74a0fb753f649c584cf2257c7805f Mon Sep 17 00:00:00 2001 From: ARST113 <96774211+ARST113@users.noreply.github.com> Date: Thu, 27 Aug 2026 17:34:04 +0300 Subject: [PATCH 46/52] Fix Android cross compilation for HEVC FFmpeg renderer From bc856cc41cd35aeefb0575d837a2b82fd183a3be Mon Sep 17 00:00:00 2001 From: ARST113 <96774211+ARST113@users.noreply.github.com> Date: Thu, 27 Aug 2026 17:37:30 +0300 Subject: [PATCH 47/52] Speed CI by checking out only required FFmpeg submodule --- .github/workflows/android-build.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index 1f9acc2..0e13b84 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -21,7 +21,10 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 - submodules: recursive + submodules: false + + - name: Initialize required FFmpeg submodule + run: git submodule update --init --depth 1 native/ffmpeg-src - name: Set up JDK 17 uses: actions/setup-java@v4 From 5c0b67b6c1aefe13dce05b5042b69af401154c7d Mon Sep 17 00:00:00 2001 From: ARST113 <96774211+ARST113@users.noreply.github.com> Date: Thu, 27 Aug 2026 17:38:15 +0300 Subject: [PATCH 48/52] Disable libyuv SME for NDK 27 --- scripts/build-media3-hevc-ffmpeg.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/build-media3-hevc-ffmpeg.sh b/scripts/build-media3-hevc-ffmpeg.sh index 097dba5..3657e91 100644 --- a/scripts/build-media3-hevc-ffmpeg.sh +++ b/scripts/build-media3-hevc-ffmpeg.sh @@ -82,7 +82,10 @@ cmake .. \ -DANDROID_PLATFORM=android-$API \ -DCMAKE_BUILD_TYPE=Release \ -DBUILD_SHARED_LIBS=ON \ - -DANDROID_STL=c++_shared + -DANDROID_STL=c++_shared \ + -DCAN_COMPILE_SME=FALSE \ + -DCMAKE_C_FLAGS="-DLIBYUV_DISABLE_SME" \ + -DCMAKE_CXX_FLAGS="-DLIBYUV_DISABLE_SME" cmake --build . -j2 popd >/dev/null mkdir -p "$JNI/libyuv/android-libs/arm64-v8a" From e031f68b5ae8249fb55e3e199c25990d98ab2702 Mon Sep 17 00:00:00 2001 From: ARST113 <96774211+ARST113@users.noreply.github.com> Date: Thu, 27 Aug 2026 17:41:04 +0300 Subject: [PATCH 49/52] Disable unsupported libyuv SME kernels on NDK 27 --- scripts/build-media3-hevc-ffmpeg.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/build-media3-hevc-ffmpeg.sh b/scripts/build-media3-hevc-ffmpeg.sh index 3657e91..1559a8b 100644 --- a/scripts/build-media3-hevc-ffmpeg.sh +++ b/scripts/build-media3-hevc-ffmpeg.sh @@ -74,6 +74,10 @@ popd >/dev/null echo "==> Build libyuv arm64" git clone -q --depth 1 https://chromium.googlesource.com/libyuv/libyuv "$JNI/libyuv" +# NDK 27/Clang 18 can pass libyuv's C SME probe but cannot compile the C++ +# __arm_new("za") implementation used by current rotate_sme.cc. SME is only +# an optimization; disable that object set and keep NEON/SVE paths. +sed -i 's/if (CAN_COMPILE_SME)/if (FALSE)/' "$JNI/libyuv/CMakeLists.txt" mkdir -p "$JNI/libyuv/build-arm64-v8a" pushd "$JNI/libyuv/build-arm64-v8a" >/dev/null cmake .. \ From 1b98883d09e494d5eabce06a3f06b42834b419c9 Mon Sep 17 00:00:00 2001 From: ARST113 <96774211+ARST113@users.noreply.github.com> Date: Thu, 27 Aug 2026 17:48:57 +0300 Subject: [PATCH 50/52] Build HEVC FFmpeg extension only for arm64-v8a --- scripts/build-media3-hevc-ffmpeg.sh | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/scripts/build-media3-hevc-ffmpeg.sh b/scripts/build-media3-hevc-ffmpeg.sh index 1559a8b..ff2dc63 100644 --- a/scripts/build-media3-hevc-ffmpeg.sh +++ b/scripts/build-media3-hevc-ffmpeg.sh @@ -95,6 +95,19 @@ popd >/dev/null mkdir -p "$JNI/libyuv/android-libs/arm64-v8a" cp "$JNI/libyuv/build-arm64-v8a/libyuv.so" "$JNI/libyuv/android-libs/arm64-v8a/libyuv.so" +echo "==> Limit FFmpeg extension to the ABI actually built for this transition APK" +python3 - <<'PY' +from pathlib import Path +p = Path("libraries/decoder_ffmpeg/build.gradle") +s = p.read_text() +if "abiFilters 'arm64-v8a'" not in s: + marker = "android {\n" + if marker not in s: + raise SystemExit("decoder_ffmpeg android block not found") + s = s.replace(marker, "android {\n defaultConfig {\n ndk { abiFilters 'arm64-v8a' }\n }\n", 1) +p.write_text(s) +PY + echo "==> Build Media3 FFmpeg AAR against exact Just+ Media3 version" pushd "$MEDIA" >/dev/null chmod +x ./gradlew From fd5f9ffeff7836ca527a83f7d01e0ae95e1ea97a Mon Sep 17 00:00:00 2001 From: ARST113 <96774211+ARST113@users.noreply.github.com> Date: Thu, 27 Aug 2026 17:53:09 +0300 Subject: [PATCH 51/52] Fix Media3 module path before ABI patch --- scripts/build-media3-hevc-ffmpeg.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build-media3-hevc-ffmpeg.sh b/scripts/build-media3-hevc-ffmpeg.sh index ff2dc63..fbf35d5 100644 --- a/scripts/build-media3-hevc-ffmpeg.sh +++ b/scripts/build-media3-hevc-ffmpeg.sh @@ -96,6 +96,7 @@ mkdir -p "$JNI/libyuv/android-libs/arm64-v8a" cp "$JNI/libyuv/build-arm64-v8a/libyuv.so" "$JNI/libyuv/android-libs/arm64-v8a/libyuv.so" echo "==> Limit FFmpeg extension to the ABI actually built for this transition APK" +pushd "$MEDIA" >/dev/null python3 - <<'PY' from pathlib import Path p = Path("libraries/decoder_ffmpeg/build.gradle") @@ -109,7 +110,6 @@ p.write_text(s) PY echo "==> Build Media3 FFmpeg AAR against exact Just+ Media3 version" -pushd "$MEDIA" >/dev/null chmod +x ./gradlew ./gradlew :lib-decoder-ffmpeg:assembleRelease --stacktrace AAR="$(find libraries/decoder_ffmpeg -type f -name '*.aar' | grep -i release | head -n1)" From 2ddd07267442349df58e8b2902c38798ce311b2f Mon Sep 17 00:00:00 2001 From: ARST113 <96774211+ARST113@users.noreply.github.com> Date: Thu, 27 Aug 2026 18:04:06 +0300 Subject: [PATCH 52/52] Patch Media3 Kotlin Gradle ABI configuration --- scripts/build-media3-hevc-ffmpeg.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/build-media3-hevc-ffmpeg.sh b/scripts/build-media3-hevc-ffmpeg.sh index fbf35d5..b1a6e79 100644 --- a/scripts/build-media3-hevc-ffmpeg.sh +++ b/scripts/build-media3-hevc-ffmpeg.sh @@ -99,13 +99,13 @@ echo "==> Limit FFmpeg extension to the ABI actually built for this transition A pushd "$MEDIA" >/dev/null python3 - <<'PY' from pathlib import Path -p = Path("libraries/decoder_ffmpeg/build.gradle") +p = Path("libraries/decoder_ffmpeg/build.gradle.kts") s = p.read_text() if "abiFilters 'arm64-v8a'" not in s: marker = "android {\n" if marker not in s: raise SystemExit("decoder_ffmpeg android block not found") - s = s.replace(marker, "android {\n defaultConfig {\n ndk { abiFilters 'arm64-v8a' }\n }\n", 1) + s = s.replace(marker, "android {\n defaultConfig {\n ndk { abiFilters += listOf(\"arm64-v8a\") }\n }\n", 1) p.write_text(s) PY