diff --git a/Android/app/src/main/AndroidManifest.xml b/Android/app/src/main/AndroidManifest.xml index 335a8837..a31e29f1 100644 --- a/Android/app/src/main/AndroidManifest.xml +++ b/Android/app/src/main/AndroidManifest.xml @@ -39,9 +39,11 @@ android:name=".MainActivity" android:exported="true" android:launchMode="singleTask" + android:supportsPictureInPicture="true" + android:resizeableActivity="true" android:theme="@style/Theme.Archipelago.Splash" android:windowSoftInputMode="adjustResize" - android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"> + android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|keyboardHidden"> diff --git a/neode-ui/src/components/__tests__/MediaLightboxPip.test.ts b/neode-ui/src/components/__tests__/MediaLightboxPip.test.ts index 45ac2394..ca9e2dd3 100644 --- a/neode-ui/src/components/__tests__/MediaLightboxPip.test.ts +++ b/neode-ui/src/components/__tests__/MediaLightboxPip.test.ts @@ -2,6 +2,7 @@ import { describe, it, expect, afterEach, beforeEach, vi } from 'vitest' import { mount, flushPromises } from '@vue/test-utils' import MediaLightbox from '../cloud/MediaLightbox.vue' import { usePipSession } from '../../composables/usePipSession' +import { togglePip } from '../../utils/pip' import type { FileBrowserItem } from '../../api/filebrowser-client' // jsdom has no picture-in-picture implementation — stub the pieces the @@ -75,6 +76,18 @@ async function mountLightbox() { } describe('MediaLightbox picture-in-picture handoff', () => { + it('checks PiP support when the action is invoked in a WebView', async () => { + const video = document.createElement('video') + const request = vi.spyOn(video, 'requestPictureInPicture') + Object.defineProperty(document, 'pictureInPictureEnabled', { value: false, configurable: true }) + await togglePip(video) + expect(request).not.toHaveBeenCalled() + + Object.defineProperty(document, 'pictureInPictureEnabled', { value: true, configurable: true }) + await togglePip(video) + expect(request).toHaveBeenCalledOnce() + }) + it('entering PiP emits close exactly once and adopts the video before doing so', async () => { const wrapper = await mountLightbox() const video = findVideo() diff --git a/neode-ui/src/utils/pip.ts b/neode-ui/src/utils/pip.ts index 7399a980..0d1292a6 100644 --- a/neode-ui/src/utils/pip.ts +++ b/neode-ui/src/utils/pip.ts @@ -20,7 +20,10 @@ export function isPipSupported(): boolean { } export async function togglePip(video: HTMLVideoElement | null | undefined): Promise { - if (!video || !pipSupported) return + // Check at call time. Embedded WebViews can expose the document capability + // after the module has been evaluated, and companion navigation can restore + // a page with a different media capability than its first load. + if (!video || !isPipSupported()) return try { if (document.pictureInPictureElement === video) await document.exitPictureInPicture() else await video.requestPictureInPicture()