Files
archy/neode-ui/src/composables/usePipSession.ts
T

76 lines
2.2 KiB
TypeScript

import { ref, readonly } from 'vue'
/** Singleton picture-in-picture session.
*
* A video handed to `adopt()` is moved into a body-level custodial host so
* that it survives the unmount of whatever view rendered it — a Teleport
* and a KeepAlive'd view both move their subtree on deactivation, and a
* moved element is a removed element as far as the picture-in-picture spec
* is concerned. Owning the element at the document level sidesteps that
* entirely: nothing about where the video *used* to live can touch it once
* it has been adopted.
*
* Module singleton, no Vue lifecycle hooks — a lifecycle hook in a bare
* composable silently no-ops outside a component's setup(). */
const active = ref(false)
const element = ref<HTMLVideoElement | null>(null)
let host: HTMLDivElement | null = null
let leaveHandler: (() => void) | null = null
function ensureHost(): HTMLDivElement {
if (host) return host
const el = document.createElement('div')
el.setAttribute('data-pip-session-host', '')
el.setAttribute('aria-hidden', 'true')
el.style.position = 'fixed'
el.style.top = '0'
el.style.left = '0'
el.style.width = '1px'
el.style.height = '1px'
el.style.overflow = 'hidden'
el.style.opacity = '0'
el.style.pointerEvents = 'none'
el.style.zIndex = '-1'
document.body.appendChild(el)
host = el
return el
}
function release(): void {
const video = element.value
if (video) {
if (leaveHandler) video.removeEventListener('leavepictureinpicture', leaveHandler)
video.pause()
video.removeAttribute('src')
video.src = ''
video.load()
if (video.parentNode) video.parentNode.removeChild(video)
}
leaveHandler = null
element.value = null
active.value = false
}
function adopt(video: HTMLVideoElement): void {
if (active.value && element.value && element.value !== video) {
release()
}
const hostEl = ensureHost()
hostEl.appendChild(video)
element.value = video
active.value = true
leaveHandler = () => release()
video.addEventListener('leavepictureinpicture', leaveHandler)
}
export function usePipSession() {
return {
active: readonly(active),
element: readonly(element),
adopt,
release,
}
}