影音體驗升級:掌握 Picture-in-Picture 與 Fullscreen API
在開發影音播放器或需要高專注度的應用時,全螢幕 (Fullscreen) 與 畫中畫 (Picture-in-Picture, PiP) 是提升使用者體驗的兩大利器。前者能提供沉浸式感受,後者則能讓使用者在切換分頁或執行其他工作時,依然能持續觀看影片。
一、 Picture-in-Picture (PiP)
1.1 API 概念
PiP API 專門用於 <video> 元素。它會將影片從瀏覽器頁面「彈出」,顯示在一個置頂的小視窗中。
- 觸發限制:必須由「使用者互動」(如:點擊事件)觸發。
- 核心方法:
video.requestPictureInPicture(): 進入畫中畫。document.exitPictureInPicture(): 退出所有畫中畫視窗。
1.2 封裝 usePiP Composable
typescript
// composables/usePiP.ts
import { ref, onMounted } from 'vue';
export function usePiP(videoRef: Ref<HTMLVideoElement | null>) {
const isSupported = typeof document !== 'undefined' && 'pictureInPictureEnabled' in document;
const isPiP = ref(false);
async function enterPiP() {
if (!videoRef.value || !isSupported) return;
try {
await videoRef.value.requestPictureInPicture();
} catch (error) {
console.error('進入 PiP 失敗:', error);
}
}
async function exitPiP() {
if (!document.pictureInPictureElement) return;
try {
await document.exitPictureInPicture();
} catch (error) {
console.error('退出 PiP 失敗:', error);
}
}
async function togglePiP() {
isPiP.value ? await exitPiP() : await enterPiP();
}
// 監聽原生事件以同步狀態
onMounted(() => {
const video = videoRef.value;
if (!video) return;
video.addEventListener('enterpictureinpicture', () => (isPiP.value = true));
video.addEventListener('leavepictureinpicture', () => (isPiP.value = false));
});
return { isSupported, isPiP, enterPiP, exitPiP, togglePiP };
}二、 Fullscreen API
2.1 API 概念
與 PiP 不同,Fullscreen API 可以作用於「任何」元素(如:整個播放器容器、畫布、甚至是某張圖片)。
- 觸發限制:同樣必須由使用者互動觸發。
- 核心方法:
element.requestFullscreen(): 指定元素進入全螢幕。document.exitFullscreen(): 退出全螢幕。
2.2 封裝 useFullscreen Composable
typescript
// composables/useFullscreen.ts
import { ref, onMounted, onUnmounted } from 'vue';
export function useFullscreen(targetRef: Ref<HTMLElement | null>) {
const isSupported = typeof document !== 'undefined' && 'fullscreenEnabled' in document;
const isFullscreen = ref(false);
async function enterFullscreen() {
if (!targetRef.value || !isSupported) return;
try {
await targetRef.value.requestFullscreen();
} catch (error) {
console.error('進入全螢幕失敗:', error);
}
}
async function exitFullscreen() {
if (!document.fullscreenElement) return;
try {
await document.exitFullscreen();
} catch (error) {
console.error('退出全螢幕失敗:', error);
}
}
function toggleFullscreen() {
isFullscreen.value ? exitFullscreen() : enterFullscreen();
}
const handler = () => {
isFullscreen.value = !!document.fullscreenElement;
};
onMounted(() => {
document.addEventListener('fullscreenchange', handler);
});
onUnmounted(() => {
document.removeEventListener('fullscreenchange', handler);
});
return { isSupported, isFullscreen, enterFullscreen, exitFullscreen, toggleFullscreen };
}三、 實戰範例:自定義影片控制項
vue
<script setup lang="ts">
import { ref } from 'vue';
import { usePiP } from './usePiP';
import { useFullscreen } from './useFullscreen';
const videoRef = ref<HTMLVideoElement | null>(null);
const containerRef = ref<HTMLElement | null>(null);
const { isPiP, togglePiP } = usePiP(videoRef);
const { isFullscreen, toggleFullscreen } = useFullscreen(containerRef);
</script>
<template>
<div ref="containerRef" class="player-container">
<video ref="videoRef" src="/demo-video.mp4" controls />
<div class="custom-controls">
<button @click="togglePiP">
{{ isPiP ? '退出畫中畫' : '進入畫中畫' }}
</button>
<button @click="toggleFullscreen">
{{ isFullscreen ? '退出全螢幕' : '全螢幕模式' }}
</button>
</div>
</div>
</template>
<style scoped>
.player-container {
position: relative;
background: #000;
}
.custom-controls {
padding: 10px;
display: flex;
gap: 10px;
}
</style>四、 技術注意事項
- 使用者啟動 (User Activation):如果你在
setTimeout或非同步回呼中呼叫requestFullscreen或requestPictureInPicture,可能會因為失去了「使用者手勢」而失敗。 - CSS 偽類:在全螢幕模式下,你可以使用
:fullscreen偽類來調整樣式。 - 影音串流 (Media Streams):PiP 視窗在影片暫停或串流結束時,行為可能因瀏覽器而異。
總結
Picture-in-Picture 與 Fullscreen API 是提升影音應用專業度的不二法門。透過 Composable 的封裝,我們能輕鬆在 Vue 元件中追蹤狀態(如:按鈕文字隨狀態切換),讓程式碼既乾淨又易於維護。