跳至主要內容
Skip to content

影音體驗升級:掌握 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>

四、 技術注意事項

  1. 使用者啟動 (User Activation):如果你在 setTimeout 或非同步回呼中呼叫 requestFullscreenrequestPictureInPicture,可能會因為失去了「使用者手勢」而失敗。
  2. CSS 偽類:在全螢幕模式下,你可以使用 :fullscreen 偽類來調整樣式。
  3. 影音串流 (Media Streams):PiP 視窗在影片暫停或串流結束時,行為可能因瀏覽器而異。

總結

Picture-in-Picture 與 Fullscreen API 是提升影音應用專業度的不二法門。透過 Composable 的封裝,我們能輕鬆在 Vue 元件中追蹤狀態(如:按鈕文字隨狀態切換),讓程式碼既乾淨又易於維護。


延伸閱讀