Skip to content

Commit 20c9ae3

Browse files
authored
Merge pull request #162 from KuekHaoYang/codex/fix-android-pip-issue-159
Fix Android APK PiP capture for inline and system fullscreen
2 parents f93f3ab + c1bcb51 commit 20c9ae3

4 files changed

Lines changed: 407 additions & 69 deletions

File tree

android-tv/app/src/main/java/com/kvideo/tv/MainActivity.kt

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import android.annotation.SuppressLint
44
import android.content.Context
55
import android.content.pm.PackageManager
66
import android.content.res.Configuration
7+
import android.graphics.Rect
78
import android.net.Uri
89
import android.os.Build
910
import android.os.Bundle
@@ -25,6 +26,10 @@ import android.widget.EditText
2526
import android.widget.FrameLayout
2627
import android.widget.TextView
2728
import androidx.activity.ComponentActivity
29+
import java.util.concurrent.CountDownLatch
30+
import java.util.concurrent.TimeUnit
31+
import java.util.concurrent.atomic.AtomicBoolean
32+
import kotlin.math.roundToInt
2833

2934
class MainActivity : ComponentActivity() {
3035

@@ -210,6 +215,7 @@ class MainActivity : ComponentActivity() {
210215
newConfig: Configuration
211216
) {
212217
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
218+
dispatchPictureInPictureChange(isInPictureInPictureMode)
213219
if (!isInPictureInPictureMode) {
214220
applyImmersiveMode()
215221
}
@@ -338,30 +344,82 @@ class MainActivity : ComponentActivity() {
338344
return packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)
339345
}
340346

347+
private fun dispatchPictureInPictureChange(isInPictureInPictureMode: Boolean) {
348+
val js = """
349+
window.dispatchEvent(new CustomEvent('kvideo-android-pip-change', {
350+
detail: { inPictureInPicture: ${if (isInPictureInPictureMode) "true" else "false"} }
351+
}));
352+
""".trimIndent()
353+
354+
webView.post {
355+
try {
356+
webView.evaluateJavascript(js, null)
357+
} catch (error: Throwable) {
358+
Log.w(TAG, "Failed to dispatch Picture-in-Picture change event", error)
359+
}
360+
}
361+
}
362+
363+
private fun createSourceRectHint(left: Int, top: Int, right: Int, bottom: Int): Rect? {
364+
if (right <= left || bottom <= top) {
365+
return null
366+
}
367+
368+
val density = resources.displayMetrics.density
369+
return Rect(
370+
(left * density).roundToInt(),
371+
(top * density).roundToInt(),
372+
(right * density).roundToInt(),
373+
(bottom * density).roundToInt()
374+
)
375+
}
376+
341377
private inner class AndroidPlayerBridge {
342378
@JavascriptInterface
343379
fun isPictureInPictureSupported(): Boolean = this@MainActivity.isPictureInPictureSupported()
344380

345381
@JavascriptInterface
346-
fun enterPictureInPicture(width: Int, height: Int): Boolean {
382+
fun enterPictureInPicture(
383+
width: Int,
384+
height: Int,
385+
left: Int,
386+
top: Int,
387+
right: Int,
388+
bottom: Int
389+
): Boolean {
347390
if (!this@MainActivity.isPictureInPictureSupported()) {
348391
return false
349392
}
350393

394+
val didEnterPiP = AtomicBoolean(false)
395+
val latch = CountDownLatch(1)
396+
351397
runOnUiThread {
352398
try {
353-
exitCustomFullscreen()
354399
val builder = android.app.PictureInPictureParams.Builder()
355400
if (width > 0 && height > 0) {
356401
builder.setAspectRatio(Rational(width, height))
357402
}
358-
enterPictureInPictureMode(builder.build())
403+
createSourceRectHint(left, top, right, bottom)?.let(builder::setSourceRectHint)
404+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
405+
builder.setSeamlessResizeEnabled(true)
406+
}
407+
didEnterPiP.set(enterPictureInPictureMode(builder.build()))
359408
} catch (error: IllegalStateException) {
360409
Log.w(TAG, "Failed to enter Picture-in-Picture mode", error)
410+
} finally {
411+
latch.countDown()
361412
}
362413
}
363414

364-
return true
415+
return try {
416+
latch.await(1500, TimeUnit.MILLISECONDS)
417+
didEnterPiP.get()
418+
} catch (error: InterruptedException) {
419+
Thread.currentThread().interrupt()
420+
Log.w(TAG, "Interrupted while waiting for Picture-in-Picture result", error)
421+
false
422+
}
365423
}
366424
}
367425
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import type { FullscreenMode } from '../useDesktopPlayerState';
2+
3+
export interface AndroidPiPTransitionPlan {
4+
enterTemporaryWindowFullscreen: boolean;
5+
restoreInlineOnExit: boolean;
6+
}
7+
8+
export interface AndroidPiPSessionState {
9+
enteredTemporaryWindowFullscreen: boolean;
10+
restoreInlineOnExit: boolean;
11+
}
12+
13+
export interface AndroidPiPSourceRect {
14+
left: number;
15+
top: number;
16+
right: number;
17+
bottom: number;
18+
}
19+
20+
export function createAndroidPiPTransitionPlan(fullscreenMode: FullscreenMode): AndroidPiPTransitionPlan {
21+
const enterTemporaryWindowFullscreen = fullscreenMode !== 'window';
22+
23+
return {
24+
enterTemporaryWindowFullscreen,
25+
restoreInlineOnExit: enterTemporaryWindowFullscreen,
26+
};
27+
}
28+
29+
export function shouldRestoreInlineAfterAndroidPiP(
30+
session: AndroidPiPSessionState | null,
31+
inPictureInPicture: boolean
32+
): boolean {
33+
if (!session || inPictureInPicture) {
34+
return false;
35+
}
36+
37+
return session.enteredTemporaryWindowFullscreen && session.restoreInlineOnExit;
38+
}
39+
40+
export function shouldRollbackTemporaryWindowFullscreen(session: AndroidPiPSessionState | null): boolean {
41+
return Boolean(session?.enteredTemporaryWindowFullscreen);
42+
}
43+
44+
export function getAndroidPiPSourceRect(container: HTMLElement | null): AndroidPiPSourceRect | null {
45+
if (!container) {
46+
return null;
47+
}
48+
49+
const rect = container.getBoundingClientRect();
50+
if (rect.width <= 0 || rect.height <= 0) {
51+
return null;
52+
}
53+
54+
return {
55+
left: Math.max(0, Math.round(rect.left)),
56+
top: Math.max(0, Math.round(rect.top)),
57+
right: Math.max(0, Math.round(rect.right)),
58+
bottom: Math.max(0, Math.round(rect.bottom)),
59+
};
60+
}

0 commit comments

Comments
 (0)