feat: add Document Picture-in-Picture plugin for ArtPlayer

- Implemented the core functionality of the Document Picture-in-Picture plugin.
- Created main plugin file `artplayer-plugin-document-pip.js` with support for opening and closing the PIP window.
- Added legacy support in `artplayer-plugin-document-pip.legacy.js`.
- Defined plugin options in `package.json` and included necessary metadata.
- Developed the main logic in `src/index.js`, including style copying and event handling.
- Created TypeScript definitions in `types/artplayer-plugin-document-pip.d.ts` for better type safety.
This commit is contained in:
Harvey Zack committed 2025-08-16 16:39:58 +08:00
1 parent c23520a4a6
commit 7d4175efbc
73 files changed
+722 -230

No files matched your search

@@ -0,0 +1,194 @@
export default function artplayerPluginDocumentPip(userOptions = {}) {
const options = {
width: 480,
height: 270,
fallbackToVideoPiP: true,
placeholder: `Playing in Document Picture-in-Picture`,
...userOptions,
}
return (art) => {
const dpipOK = 'documentPictureInPicture' in window
&& typeof window.documentPictureInPicture?.requestWindow === 'function'
const { proxy, icons, i18n, template: { $container }, constructor: { utils: { append, tooltip, addClass, removeClass } } } = art
const state = {
win: null,
originalParent: null,
originalNext: null,
currentDoc: null,
placeholder: null,
}
function copyStylesTo(targetDoc) {
const base = targetDoc.createElement('style')
base.textContent = `
html, body { margin:0; padding:0; width:100%; height:100%; background:#000; overflow:hidden; }
#__art_dpip_root { position:absolute; inset:0; display:flex; }
#__art_dpip_root > * { width:100% !important; height:100% !important; }
`
targetDoc.head.appendChild(base)
if (!targetDoc.querySelector('meta[name="viewport"]')) {
const meta = targetDoc.createElement('meta')
meta.name = 'viewport'
meta.content = 'width=device-width,initial-scale=1'
targetDoc.head.appendChild(meta)
}
try {
document.querySelectorAll('link[rel="stylesheet"]').forEach((link) => {
const clone = targetDoc.createElement('link')
clone.rel = 'stylesheet'
clone.href = link.href
if (link.media)
clone.media = link.media
if (link.crossOrigin)
clone.crossOrigin = link.crossOrigin
if (link.referrerPolicy)
clone.referrerPolicy = link.referrerPolicy
targetDoc.head.appendChild(clone)
})
}
catch { }
try {
document.querySelectorAll('style').forEach((style) => {
const clone = targetDoc.createElement('style')
clone.textContent = style.textContent
targetDoc.head.appendChild(clone)
})
}
catch { }
}
async function open() {
if (!dpipOK && options.fallbackToVideoPiP) {
art.pip = true
console.warn('[artplayer-plugin-document-pip] Document Picture-in-Picture is not supported, falling back to Video Picture-in-Picture')
return
}
if (state.win)
return
try {
const pipWin = await window.documentPictureInPicture.requestWindow({
width: options.width,
height: options.height,
})
state.win = pipWin
state.currentDoc = pipWin.document
state.placeholder = document.createElement('div')
state.placeholder.className = 'artplayer-pip-placeholder'
state.placeholder.style.cssText = `display:flex;justify-content:center;align-items:center;width:100%;height:100%;`
state.placeholder.textContent = options.placeholder
state.originalParent = $container.parentNode
state.originalNext = $container.nextSibling
state.originalParent.insertBefore(state.placeholder, state.originalNext)
const root = pipWin.document.createElement('div')
root.id = '__art_dpip_root'
pipWin.document.body.appendChild(root)
copyStylesTo(pipWin.document)
const adopted = pipWin.document.adoptNode($container)
root.appendChild(adopted)
state.currentDoc = pipWin.document
const onResize = () => art.resize?.()
const onClose = () => close()
pipWin.addEventListener('resize', onResize)
pipWin.addEventListener('pagehide', onClose)
pipWin.addEventListener('unload', onClose)
state.cleanup = () => {
pipWin.removeEventListener('resize', onResize)
pipWin.removeEventListener('pagehide', onClose)
pipWin.removeEventListener('unload', onClose)
}
addClass($container, 'artplayer-document-pip')
art.emit('document-pip', true)
}
catch (err) {
art.notice.show = 'Document Picture-in-Picture open failed'
console.warn('[artplayer-plugin-document-pip] open failed:', err)
}
}
function restoreToOriginalDocument() {
if (!state.placeholder)
return
const backDoc = state.placeholder.ownerDocument || document
const adoptedBack = backDoc.adoptNode($container)
state.originalParent.insertBefore(adoptedBack, state.placeholder)
state.placeholder.remove()
state.placeholder = null
state.currentDoc = backDoc
}
function close() {
if (!dpipOK && options.fallbackToVideoPiP) {
art.pip = false
return
}
if (!state.win)
return
try {
state.cleanup?.()
restoreToOriginalDocument()
state.win.close()
state.win = null
removeClass($container, 'artplayer-document-pip')
art.emit('document-pip', false)
}
catch (err) {
art.notice.show = 'Document Picture-in-Picture close failed'
console.warn('[artplayer-plugin-document-pip] close failed:', err)
}
}
function toggle() {
if (state.win) {
close()
}
else {
open()
}
}
art.controls.add({
name: 'document-pip',
position: 'right',
index: 40,
tooltip: art.i18n.get('PIP Mode'),
mounted: ($control) => {
append($control, icons.pip)
proxy($control, 'click', toggle)
art.on('document-pip', (value) => {
tooltip($control, i18n.get(value ? 'Exit PIP Mode' : 'PIP Mode'))
})
},
})
art.on('destroy', close)
return {
name: 'artplayerPluginDocumentPip',
state,
open,
close,
toggle,
}
}
}
if (typeof window !== 'undefined') {
window.artplayerPluginDocumentPip = artplayerPluginDocumentPip
}