Notification and Messaging APIs

1. Notification API for Desktop Notifications

Method/Property Description Browser Support
Notification.permission Current permission state: "granted", "denied", or "default". All Modern Browsers
Notification.requestPermission() Requests notification permission. Returns Promise resolving to permission state. All Modern Browsers
new Notification(title, options) Creates and displays notification. Options include body, icon, badge, tag, etc. All Modern Browsers
notification.close() Closes notification programmatically. All Modern Browsers
Notification Option Type Description
body string Notification body text.
icon string URL to icon image. Typically 192x192px.
badge string URL to badge icon for notification area. Monochrome, 96x96px.
tag string ID for notification grouping. Same tag replaces previous notification.
data any Arbitrary data to associate with notification. Accessible in event handlers.
requireInteraction boolean If true, notification persists until user interacts. Default: false.
actions array Array of action objects with action, title, icon. Service Worker only.
silent boolean If true, no sound/vibration. Default: false.
vibrate array Vibration pattern: [200, 100, 200] (vibrate 200ms, pause 100ms, vibrate 200ms).

Example: Request permission and show notification

// Check if notifications are supported
if (!("Notification" in window)) {
  console.log("Notifications not supported");
} else {
  console.log("Current permission:", Notification.permission);
}

// Request permission
async function requestNotificationPermission() {
  if (Notification.permission === "granted") {
    console.log("Permission already granted");
    return true;
  }
  
  if (Notification.permission === "denied") {
    console.log("Permission denied - cannot request again");
    return false;
  }
  
  // Request permission (must be from user gesture)
  const permission = await Notification.requestPermission();
  
  if (permission === "granted") {
    console.log("Permission granted!");
    return true;
  } else {
    console.log("Permission denied");
    return false;
  }
}

// Show notification
function showNotification() {
  if (Notification.permission !== "granted") {
    console.log("No permission to show notifications");
    return;
  }
  
  const notification = new Notification("New Message", {
    "body": "You have a new message from John",
    "icon": "/images/icon-192.png",
    "badge": "/images/badge-96.png",
    "tag": "message-123", // Replace previous notification with same tag
    "data": {
      "messageId": "123",
      "userId": "456"
    },
    "requireInteraction": false,
    "silent": false,
    "vibrate": [200, 100, 200]
  });
  
  // Event handlers
  notification.onclick = (event) => {
    console.log("Notification clicked:", event);
    window.focus();
    notification.close();
    
    // Navigate to message
    const { "messageId": messageId } = notification.data;
    window.location.href = `/messages/${messageId}`;
  };
  
  notification.onclose = (event) => {
    console.log("Notification closed");
  };
  
  notification.onerror = (event) => {
    console.error("Notification error:", event);
  };
  
  notification.onshow = (event) => {
    console.log("Notification shown");
  };
  
  // Auto-close after 5 seconds
  setTimeout(() => {
    notification.close();
  }, 5000);
}

// Usage - must be from user interaction
document.getElementById("notifyBtn").addEventListener("click", async () => {
  const hasPermission = await requestNotificationPermission();
  if (hasPermission) {
    showNotification();
  }
});

Example: Notification with actions (Service Worker)

// service-worker.js - Show notification with actions
self.addEventListener("push", (event) => {
  const data = event.data.json();
  
  const options = {
    "body": data.body,
    "icon": "/images/icon-192.png",
    "badge": "/images/badge-96.png",
    "tag": data.tag,
    "data": data,
    "actions": [
      {
        "action": "view",
        "title": "View",
        "icon": "/images/view-icon.png"
      },
      {
        "action": "dismiss",
        "title": "Dismiss",
        "icon": "/images/dismiss-icon.png"
      }
    ],
    "requireInteraction": true
  };
  
  event.waitUntil(
    self.registration.showNotification(data.title, options)
  );
});

// Handle notification click
self.addEventListener("notificationclick", (event) => {
  console.log("Notification clicked:", event.action);
  
  event.notification.close();
  
  if (event.action === "view") {
    // Open app to specific page
    event.waitUntil(
      clients.openWindow(`/app?id=${event.notification.data.id}`)
    );
  } else if (event.action === "dismiss") {
    // Just close - no action needed
    console.log("Notification dismissed");
  } else {
    // Default click (no action button)
    event.waitUntil(
      clients.openWindow("/app")
    );
  }
});

// Handle notification close
self.addEventListener("notificationclose", (event) => {
  console.log("Notification closed:", event.notification.tag);
  
  // Track analytics
  event.waitUntil(
    fetch("/analytics/notification-closed", {
      "method": "POST",
      "body": JSON.stringify({
        "tag": event.notification.tag,
        "timestamp": Date.now()
      })
    })
  );
});
Note: Notification API shows desktop notifications to users. Requires user permission - request from user gesture. Use tag to replace/update existing notifications. Service Worker notifications support action buttons. Always handle permission denial gracefully.
Warning: Must request permission from user gesture (click). Permission is per-origin. If denied, cannot request again - user must manually enable. Don't spam notifications. Respect requireInteraction - don't force persistent notifications. Test across browsers - behavior varies.

2. Push Messaging Registration and Handling

Method/Property Description Browser Support
registration.pushManager.subscribe(options) Subscribes to push notifications. Returns PushSubscription with endpoint and keys. All Modern Browsers
registration.pushManager.getSubscription() Gets existing push subscription or null if not subscribed. All Modern Browsers
subscription.unsubscribe() Unsubscribes from push notifications. All Modern Browsers
subscription.endpoint Push service URL for sending notifications. All Modern Browsers
subscription.getKey(name) Gets encryption key. Name: "p256dh" (public key) or "auth" (auth secret). All Modern Browsers

Example: Subscribe to push notifications

// Subscribe to push notifications
async function subscribeToPush() {
  try {
    // Check support
    if (!("serviceWorker" in navigator) || !("PushManager" in window)) {
      throw new Error("Push notifications not supported");
    }
    
    // Register service worker
    const registration = await navigator.serviceWorker.register("/sw.js");
    await navigator.serviceWorker.ready;
    
    // Check existing subscription
    let subscription = await registration.pushManager.getSubscription();
    
    if (subscription) {
      console.log("Already subscribed:", subscription);
      return subscription;
    }
    
    // Request notification permission
    const permission = await Notification.requestPermission();
    if (permission !== "granted") {
      throw new Error("Notification permission denied");
    }
    
    // Subscribe to push
    // VAPID public key from your server
    const vapidPublicKey = "YOUR_VAPID_PUBLIC_KEY";
    const convertedVapidKey = urlBase64ToUint8Array(vapidPublicKey);
    
    subscription = await registration.pushManager.subscribe({
      "userVisibleOnly": true, // Must be true
      "applicationServerKey": convertedVapidKey
    });
    
    console.log("Push subscription:", subscription);
    
    // Send subscription to server
    await fetch("/api/push/subscribe", {
      "method": "POST",
      "headers": { "Content-Type": "application/json" },
      "body": JSON.stringify({
        "endpoint": subscription.endpoint,
        "keys": {
          "p256dh": arrayBufferToBase64(subscription.getKey("p256dh")),
          "auth": arrayBufferToBase64(subscription.getKey("auth"))
        }
      })
    });
    
    console.log("Subscription sent to server");
    return subscription;
    
  } catch (error) {
    console.error("Push subscription failed:", error);
    throw error;
  }
}

// Helper: Convert VAPID key
function urlBase64ToUint8Array(base64String) {
  const padding = "=".repeat((4 - base64String.length % 4) % 4);
  const base64 = (base64String + padding)
    .replace(/\-/g, "+")
    .replace(/_/g, "/");
  
  const rawData = atob(base64);
  const outputArray = new Uint8Array(rawData.length);
  
  for (let i = 0; i < rawData.length; ++i) {
    outputArray[i] = rawData.charCodeAt(i);
  }
  return outputArray;
}

// Helper: Convert ArrayBuffer to base64
function arrayBufferToBase64(buffer) {
  const bytes = new Uint8Array(buffer);
  let binary = "";
  for (let i = 0; i < bytes.length; i++) {
    binary += String.fromCharCode(bytes[i]);
  }
  return btoa(binary);
}

// Unsubscribe
async function unsubscribeFromPush() {
  const registration = await navigator.serviceWorker.ready;
  const subscription = await registration.pushManager.getSubscription();
  
  if (subscription) {
    await subscription.unsubscribe();
    console.log("Unsubscribed from push");
    
    // Notify server
    await fetch("/api/push/unsubscribe", {
      "method": "POST",
      "headers": { "Content-Type": "application/json" },
      "body": JSON.stringify({
        "endpoint": subscription.endpoint
      })
    });
  }
}

Example: Handle push messages in service worker

// service-worker.js
self.addEventListener("push", (event) => {
  console.log("Push received");
  
  let notificationData = {
    "title": "New Notification",
    "body": "You have a new update",
    "icon": "/images/icon-192.png"
  };
  
  // Parse push data if available
  if (event.data) {
    try {
      notificationData = event.data.json();
    } catch (e) {
      notificationData.body = event.data.text();
    }
  }
  
  const options = {
    "body": notificationData.body,
    "icon": notificationData.icon || "/images/icon-192.png",
    "badge": "/images/badge-96.png",
    "tag": notificationData.tag || "default",
    "data": notificationData.data || {},
    "actions": notificationData.actions || [],
    "requireInteraction": notificationData.requireInteraction || false,
    "vibrate": [200, 100, 200]
  };
  
  // Show notification
  event.waitUntil(
    self.registration.showNotification(notificationData.title, options)
  );
});

// Handle notification click
self.addEventListener("notificationclick", (event) => {
  event.notification.close();
  
  const urlToOpen = event.notification.data.url || "/";
  
  event.waitUntil(
    clients.matchAll({
      "type": "window",
      "includeUncontrolled": true
    }).then((clientList) => {
      // Check if already open
      for (const client of clientList) {
        if (client.url === urlToOpen && "focus" in client) {
          return client.focus();
        }
      }
      
      // Open new window
      if (clients.openWindow) {
        return clients.openWindow(urlToOpen);
      }
    })
  );
});

// Handle push subscription change
self.addEventListener("pushsubscriptionchange", (event) => {
  console.log("Push subscription changed");
  
  event.waitUntil(
    // Resubscribe
    self.registration.pushManager.subscribe({
      "userVisibleOnly": true,
      "applicationServerKey": urlBase64ToUint8Array(VAPID_PUBLIC_KEY)
    }).then((subscription) => {
      // Send new subscription to server
      return fetch("/api/push/subscribe", {
        "method": "POST",
        "headers": { "Content-Type": "application/json" },
        "body": JSON.stringify({
          "endpoint": subscription.endpoint,
          "keys": {
            "p256dh": arrayBufferToBase64(subscription.getKey("p256dh")),
            "auth": arrayBufferToBase64(subscription.getKey("auth"))
          }
        })
      });
    })
  );
});
Note: Push API enables server-to-client push notifications. Requires Service Worker and notification permission. Use VAPID keys for authentication. Subscription includes endpoint and encryption keys - send to your server. Server uses Web Push protocol to send notifications.
Warning: Requires HTTPS and Service Worker. Must set userVisibleOnly: true - silent push not allowed. Handle pushsubscriptionchange event - subscriptions can expire. Don't send sensitive data in push payload - encrypt or fetch from server. Test subscription renewal flow.

3. Badge API for Application Badge Updates

Method Description Browser Support
navigator.setAppBadge(count) Sets app icon badge count. Omit count for generic badge indicator. Chrome, Edge, Safari
navigator.clearAppBadge() Clears app icon badge. Chrome, Edge, Safari

Example: Update app badge

// Check if Badge API is supported
if ("setAppBadge" in navigator) {
  console.log("Badge API supported");
} else {
  console.log("Badge API not supported");
}

// Set badge with count
async function updateBadge(count) {
  try {
    if ("setAppBadge" in navigator) {
      if (count > 0) {
        await navigator.setAppBadge(count);
        console.log(`Badge set to ${count}`);
      } else {
        await navigator.clearAppBadge();
        console.log("Badge cleared");
      }
    }
  } catch (error) {
    console.error("Failed to update badge:", error);
  }
}

// Set generic badge (no number)
async function setGenericBadge() {
  try {
    if ("setAppBadge" in navigator) {
      await navigator.setAppBadge();
      console.log("Generic badge set");
    }
  } catch (error) {
    console.error("Failed to set badge:", error);
  }
}

// Clear badge
async function clearBadge() {
  try {
    if ("clearAppBadge" in navigator) {
      await navigator.clearAppBadge();
      console.log("Badge cleared");
    }
  } catch (error) {
    console.error("Failed to clear badge:", error);
  }
}

// Example: Update badge based on unread messages
let unreadCount = 0;

function receiveMessage(message) {
  unreadCount++;
  updateBadge(unreadCount);
  displayMessage(message);
}

function markAsRead() {
  unreadCount = Math.max(0, unreadCount - 1);
  updateBadge(unreadCount);
}

function markAllAsRead() {
  unreadCount = 0;
  clearBadge();
}

Example: Badge API in Service Worker

// service-worker.js - Update badge on push
self.addEventListener("push", (event) => {
  const data = event.data.json();
  
  // Show notification
  const notificationPromise = self.registration.showNotification(
    data.title,
    {
      "body": data.body,
      "icon": "/images/icon-192.png",
      "badge": "/images/badge-96.png",
      "tag": data.tag,
      "data": data
    }
  );
  
  // Update badge
  const badgePromise = (async () => {
    if ("setAppBadge" in navigator) {
      // Get current unread count from data
      const unreadCount = data.unreadCount || 1;
      await navigator.setAppBadge(unreadCount);
    }
  })();
  
  event.waitUntil(
    Promise.all([notificationPromise, badgePromise])
  );
});

// Clear badge when notification clicked
self.addEventListener("notificationclick", (event) => {
  event.notification.close();
  
  const clearBadgePromise = (async () => {
    if ("clearAppBadge" in navigator) {
      await navigator.clearAppBadge();
    }
  })();
  
  const openWindowPromise = clients.openWindow(
    event.notification.data.url || "/"
  );
  
  event.waitUntil(
    Promise.all([clearBadgePromise, openWindowPromise])
  );
});
Note: Badge API sets app icon badge - visible on dock/taskbar/home screen. Works for installed PWAs. Set number for count or no argument for generic indicator. Clear when user views content. Limited browser support but good progressive enhancement.
Warning: Only works for installed PWAs - not regular websites. Limited browser support (Chrome, Edge, Safari). Always check feature support before using. Badge persists across sessions - clear appropriately. Don't use for critical notifications - combine with Notification API.

4. Wake Lock API for Screen Wake Management

Method/Property Description Browser Support
navigator.wakeLock.request(type) Requests wake lock. Type: "screen". Returns WakeLockSentinel. Chrome, Edge
wakeLock.release() Releases wake lock, allowing screen to sleep. Chrome, Edge
wakeLock.released Promise that resolves when wake lock is released. Chrome, Edge
wakeLock.type Type of wake lock ("screen"). Chrome, Edge

Example: Prevent screen sleep during video playback

// Wake Lock manager
class WakeLockManager {
  constructor() {
    this.wakeLock = null;
    this.isSupported = "wakeLock" in navigator;
  }
  
  async request() {
    if (!this.isSupported) {
      console.log("Wake Lock API not supported");
      return false;
    }
    
    try {
      this.wakeLock = await navigator.wakeLock.request("screen");
      console.log("Wake lock acquired");
      
      // Listen for release
      this.wakeLock.addEventListener("release", () => {
        console.log("Wake lock released");
        this.wakeLock = null;
      });
      
      return true;
    } catch (error) {
      console.error("Wake lock request failed:", error);
      return false;
    }
  }
  
  async release() {
    if (this.wakeLock) {
      await this.wakeLock.release();
      this.wakeLock = null;
      console.log("Wake lock manually released");
    }
  }
  
  async reacquire() {
    // Reacquire if previously active (e.g., after page visibility change)
    if (this.wakeLock && this.wakeLock.released) {
      await this.request();
    }
  }
}

// Video player with wake lock
const wakeLockManager = new WakeLockManager();
const video = document.getElementById("myVideo");

video.addEventListener("play", async () => {
  await wakeLockManager.request();
});

video.addEventListener("pause", async () => {
  await wakeLockManager.release();
});

video.addEventListener("ended", async () => {
  await wakeLockManager.release();
});

// Reacquire wake lock when page becomes visible
document.addEventListener("visibilitychange", async () => {
  if (document.visibilityState === "visible" && !video.paused) {
    await wakeLockManager.reacquire();
  }
});

// Release on page unload
window.addEventListener("beforeunload", async () => {
  await wakeLockManager.release();
});

Example: Recipe app with wake lock

// Keep screen on while cooking
class RecipeWakeLock {
  constructor() {
    this.wakeLock = null;
    this.isActive = false;
  }
  
  async enable() {
    if ("wakeLock" in navigator) {
      try {
        this.wakeLock = await navigator.wakeLock.request("screen");
        this.isActive = true;
        
        console.log("Screen will stay on during cooking");
        
        // Update UI
        document.getElementById("wakeLockBtn").textContent = "Turn Off Keep Awake";
        document.getElementById("wakeLockStatus").textContent = "Screen will stay on";
        
        // Handle release
        this.wakeLock.addEventListener("release", () => {
          console.log("Wake lock released");
          this.isActive = false;
          this.updateUI();
        });
        
      } catch (error) {
        console.error("Failed to enable wake lock:", error);
        alert("Could not keep screen on. Make sure page is visible.");
      }
    } else {
      alert("Wake Lock not supported in this browser");
    }
  }
  
  async disable() {
    if (this.wakeLock) {
      await this.wakeLock.release();
      this.isActive = false;
      this.updateUI();
    }
  }
  
  async toggle() {
    if (this.isActive) {
      await this.disable();
    } else {
      await this.enable();
    }
  }
  
  updateUI() {
    const btn = document.getElementById("wakeLockBtn");
    const status = document.getElementById("wakeLockStatus");
    
    if (this.isActive) {
      btn.textContent = "Turn Off Keep Awake";
      status.textContent = "Screen will stay on";
    } else {
      btn.textContent = "Turn On Keep Awake";
      status.textContent = "Screen may sleep";
    }
  }
}

// Initialize
const recipeWakeLock = new RecipeWakeLock();

document.getElementById("wakeLockBtn").addEventListener("click", () => {
  recipeWakeLock.toggle();
});

// Auto-enable when starting recipe
document.getElementById("startCookingBtn").addEventListener("click", () => {
  recipeWakeLock.enable();
  startRecipeTimer();
});
Note: Wake Lock API prevents screen from sleeping. Use for video playback, reading, recipes, presentations, etc. Only "screen" type currently supported. Wake lock auto-releases when page hidden or battery low. Reacquire on visibility change if needed.
Warning: Limited browser support (Chrome, Edge). Requires visible page - released when tab hidden. Can be denied by browser (low battery, user settings). Always handle request failure gracefully. Remember to release when done - drains battery. Respect user's battery.

5. Idle Detection API for User Activity Monitoring

Method/Property Description Browser Support
new IdleDetector() Creates idle detector instance. Chrome, Edge (Experimental)
detector.start(options) Starts monitoring. Options: threshold (ms) and signal (AbortSignal). Chrome, Edge (Experimental)
detector.userState Current user state: "active" or "idle". Chrome, Edge (Experimental)
detector.screenState Current screen state: "locked" or "unlocked". Chrome, Edge (Experimental)
detector.onchange Event handler for state changes. Chrome, Edge (Experimental)

Example: Detect user idle state

// Check support and request permission
async function setupIdleDetection() {
  // Check support
  if (!("IdleDetector" in window)) {
    console.log("Idle Detection API not supported");
    return;
  }
  
  // Request permission
  try {
    const permission = await IdleDetector.requestPermission();
    
    if (permission !== "granted") {
      console.log("Idle detection permission denied");
      return;
    }
    
    console.log("Idle detection permission granted");
    startIdleDetection();
    
  } catch (error) {
    console.error("Permission request failed:", error);
  }
}

// Start idle detection
async function startIdleDetection() {
  try {
    const idleDetector = new IdleDetector();
    const controller = new AbortController();
    
    // Listen for state changes
    idleDetector.addEventListener("change", () => {
      const userState = idleDetector.userState;
      const screenState = idleDetector.screenState;
      
      console.log(`User: ${userState}, Screen: ${screenState}`);
      
      if (userState === "idle") {
        handleUserIdle();
      } else if (userState === "active") {
        handleUserActive();
      }
      
      if (screenState === "locked") {
        handleScreenLocked();
      } else if (screenState === "unlocked") {
        handleScreenUnlocked();
      }
    });
    
    // Start monitoring
    await idleDetector.start({
      "threshold": 60000, // 60 seconds
      "signal": controller.signal
    });
    
    console.log("Idle detection started (threshold: 60s)");
    
    // Stop detection after 10 minutes
    setTimeout(() => {
      controller.abort();
      console.log("Idle detection stopped");
    }, 10 * 60 * 1000);
    
  } catch (error) {
    console.error("Idle detection failed:", error);
  }
}

function handleUserIdle() {
  console.log("User is idle");
  // Pause background tasks, sync, etc.
  pauseBackgroundSync();
  showIdleMessage();
}

function handleUserActive() {
  console.log("User is active");
  // Resume activities
  resumeBackgroundSync();
  hideIdleMessage();
}

function handleScreenLocked() {
  console.log("Screen locked");
  // Pause video, save work, etc.
  pauseMedia();
  autoSaveWork();
}

function handleScreenUnlocked() {
  console.log("Screen unlocked");
  // Resume activities
  resumeMedia();
}

// Initialize
setupIdleDetection();

Example: Auto-logout on idle

// Auto-logout after extended idle period
class IdleLogoutManager {
  constructor(idleThreshold = 5 * 60 * 1000, logoutDelay = 60 * 1000) {
    this.idleThreshold = idleThreshold;
    this.logoutDelay = logoutDelay;
    this.idleDetector = null;
    this.logoutTimer = null;
    this.warningShown = false;
  }
  
  async start() {
    if (!("IdleDetector" in window)) {
      console.log("Using fallback idle detection");
      this.useFallbackDetection();
      return;
    }
    
    try {
      const permission = await IdleDetector.requestPermission();
      
      if (permission !== "granted") {
        this.useFallbackDetection();
        return;
      }
      
      this.idleDetector = new IdleDetector();
      
      this.idleDetector.addEventListener("change", () => {
        if (this.idleDetector.userState === "idle") {
          this.onIdle();
        } else {
          this.onActive();
        }
      });
      
      await this.idleDetector.start({
        "threshold": this.idleThreshold
      });
      
      console.log("Idle logout protection active");
      
    } catch (error) {
      console.error("Idle detection setup failed:", error);
      this.useFallbackDetection();
    }
  }
  
  onIdle() {
    console.log("User idle - starting logout countdown");
    
    // Show warning
    this.showWarning();
    
    // Start logout timer
    this.logoutTimer = setTimeout(() => {
      this.logout();
    }, this.logoutDelay);
  }
  
  onActive() {
    console.log("User active - cancelling logout");
    
    // Cancel logout
    if (this.logoutTimer) {
      clearTimeout(this.logoutTimer);
      this.logoutTimer = null;
    }
    
    // Hide warning
    this.hideWarning();
  }
  
  showWarning() {
    if (this.warningShown) return;
    
    this.warningShown = true;
    const warning = document.getElementById("idleWarning");
    warning.style.display = "block";
    warning.textContent = `You will be logged out in ${this.logoutDelay / 1000} seconds due to inactivity`;
  }
  
  hideWarning() {
    this.warningShown = false;
    const warning = document.getElementById("idleWarning");
    warning.style.display = "none";
  }
  
  logout() {
    console.log("Logging out due to inactivity");
    
    // Clear session
    localStorage.removeItem("authToken");
    sessionStorage.clear();
    
    // Redirect to login
    window.location.href = "/login?reason=idle";
  }
  
  useFallbackDetection() {
    // Fallback: use mouse/keyboard events
    let lastActivity = Date.now();
    
    const resetTimer = () => {
      lastActivity = Date.now();
      this.hideWarning();
    };
    
    ["mousedown", "mousemove", "keypress", "scroll", "touchstart"].forEach(event => {
      document.addEventListener(event, resetTimer, true);
    });
    
    // Check periodically
    setInterval(() => {
      const idleTime = Date.now() - lastActivity;
      
      if (idleTime > this.idleThreshold) {
        this.onIdle();
      }
    }, 5000);
  }
}

// Initialize
const idleLogout = new IdleLogoutManager(
  5 * 60 * 1000, // 5 minutes idle threshold
  60 * 1000      // 1 minute warning before logout
);

idleLogout.start();
Note: Idle Detection API monitors user activity and screen lock state. Requires permission. Use for auto-logout, pause sync, save battery. Threshold is minimum idle time before detection. Experimental API - limited support.
Warning: Highly experimental - Chrome/Edge only behind flag. Requires permission. Privacy-sensitive - use responsibly. Provide fallback for unsupported browsers. Don't rely solely on this for security - implement server-side session timeout. Test thoroughly before production use.

6. Picture-in-Picture API for Video Overlay

Method/Property Description Browser Support
video.requestPictureInPicture() Requests PiP mode for video element. Returns PictureInPictureWindow. All Modern Browsers
document.exitPictureInPicture() Exits PiP mode. All Modern Browsers
document.pictureInPictureElement Currently active PiP element or null. All Modern Browsers
document.pictureInPictureEnabled Boolean indicating if PiP is available. All Modern Browsers
pipWindow.width Width of PiP window. All Modern Browsers
pipWindow.height Height of PiP window. All Modern Browsers

Example: Toggle Picture-in-Picture

// Check PiP support
if (!document.pictureInPictureEnabled) {
  console.log("Picture-in-Picture not supported");
  document.getElementById("pipBtn").disabled = true;
}

// Toggle PiP mode
async function togglePictureInPicture() {
  const video = document.getElementById("myVideo");
  
  try {
    // Exit if already in PiP
    if (document.pictureInPictureElement) {
      await document.exitPictureInPicture();
      console.log("Exited Picture-in-Picture");
      return;
    }
    
    // Enter PiP
    const pipWindow = await video.requestPictureInPicture();
    console.log("Entered Picture-in-Picture");
    console.log(`PiP window size: ${pipWindow.width}x${pipWindow.height}`);
    
    // Listen for resize
    pipWindow.addEventListener("resize", () => {
      console.log(`PiP resized: ${pipWindow.width}x${pipWindow.height}`);
    });
    
  } catch (error) {
    console.error("PiP failed:", error);
    
    if (error.name === "NotAllowedError") {
      alert("Picture-in-Picture not allowed. Check browser permissions.");
    } else if (error.name === "InvalidStateError") {
      alert("Video must be playing to enable Picture-in-Picture.");
    }
  }
}

// Add button listener
document.getElementById("pipBtn").addEventListener("click", togglePictureInPicture);

// Listen for PiP events
const video = document.getElementById("myVideo");

video.addEventListener("enterpictureinpicture", (event) => {
  console.log("Entered PiP");
  document.getElementById("pipBtn").textContent = "Exit PiP";
  
  // Update UI
  document.body.classList.add("pip-active");
});

video.addEventListener("leavepictureinpicture", (event) => {
  console.log("Left PiP");
  document.getElementById("pipBtn").textContent = "Enter PiP";
  
  // Update UI
  document.body.classList.remove("pip-active");
});

Example: Auto PiP on scroll or tab switch

// Auto-enable PiP when video scrolls out of view or tab hidden
class AutoPiPManager {
  constructor(video) {
    this.video = video;
    this.autoPiPEnabled = true;
    this.observer = null;
    
    this.setupIntersectionObserver();
    this.setupVisibilityChange();
  }
  
  setupIntersectionObserver() {
    // Detect when video scrolls out of view
    this.observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (!this.autoPiPEnabled) return;
        
        // Video scrolled out of view and is playing
        if (!entry.isIntersecting && !this.video.paused) {
          this.enterPiP();
        }
        // Video back in view - exit PiP
        else if (entry.isIntersecting && document.pictureInPictureElement) {
          this.exitPiP();
        }
      });
    }, {
      "threshold": 0.5 // 50% visibility threshold
    });
    
    this.observer.observe(this.video);
  }
  
  setupVisibilityChange() {
    // Enable PiP when switching tabs
    document.addEventListener("visibilitychange", () => {
      if (!this.autoPiPEnabled) return;
      
      if (document.hidden && !this.video.paused) {
        this.enterPiP();
      } else if (!document.hidden && document.pictureInPictureElement) {
        this.exitPiP();
      }
    });
  }
  
  async enterPiP() {
    if (document.pictureInPictureElement) return;
    
    try {
      await this.video.requestPictureInPicture();
      console.log("Auto PiP enabled");
    } catch (error) {
      console.error("Auto PiP failed:", error);
    }
  }
  
  async exitPiP() {
    if (!document.pictureInPictureElement) return;
    
    try {
      await document.exitPictureInPicture();
      console.log("Auto PiP disabled");
    } catch (error) {
      console.error("Exit PiP failed:", error);
    }
  }
  
  setAutoPiP(enabled) {
    this.autoPiPEnabled = enabled;
    console.log(`Auto PiP ${enabled ? "enabled" : "disabled"}`);
    
    // Exit PiP if disabled
    if (!enabled && document.pictureInPictureElement) {
      this.exitPiP();
    }
  }
  
  destroy() {
    if (this.observer) {
      this.observer.disconnect();
    }
  }
}

// Initialize
const video = document.getElementById("myVideo");
const autoPiP = new AutoPiPManager(video);

// Toggle auto PiP
document.getElementById("autoPiPToggle").addEventListener("change", (event) => {
  autoPiP.setAutoPiP(event.target.checked);
});

Example: Custom PiP controls

// Add custom controls to PiP window (Chrome 116+)
const video = document.getElementById("myVideo");

// Check if Media Session API is available for controls
if ("mediaSession" in navigator) {
  // Set metadata
  navigator.mediaSession.metadata = new MediaMetadata({
    "title": "My Video Title",
    "artist": "Content Creator",
    "album": "Video Series",
    "artwork": [
      { "src": "/images/artwork-96.png", "sizes": "96x96", "type": "image/png" },
      { "src": "/images/artwork-512.png", "sizes": "512x512", "type": "image/png" }
    ]
  });
  
  // Set action handlers (visible in PiP)
  navigator.mediaSession.setActionHandler("play", () => {
    video.play();
  });
  
  navigator.mediaSession.setActionHandler("pause", () => {
    video.pause();
  });
  
  navigator.mediaSession.setActionHandler("previoustrack", () => {
    playPreviousVideo();
  });
  
  navigator.mediaSession.setActionHandler("nexttrack", () => {
    playNextVideo();
  });
  
  navigator.mediaSession.setActionHandler("seekbackward", (details) => {
    video.currentTime = Math.max(video.currentTime - (details.seekOffset || 10), 0);
  });
  
  navigator.mediaSession.setActionHandler("seekforward", (details) => {
    video.currentTime = Math.min(
      video.currentTime + (details.seekOffset || 10),
      video.duration
    );
  });
}

// Update playback state
video.addEventListener("play", () => {
  navigator.mediaSession.playbackState = "playing";
});

video.addEventListener("pause", () => {
  navigator.mediaSession.playbackState = "paused";
});

// Update position
video.addEventListener("timeupdate", () => {
  if ("setPositionState" in navigator.mediaSession) {
    navigator.mediaSession.setPositionState({
      "duration": video.duration,
      "playbackRate": video.playbackRate,
      "position": video.currentTime
    });
  }
});
Note: Picture-in-Picture API enables floating video overlay. Video continues playing while user browses other tabs/apps. Must be triggered by user interaction. Use for video conferencing, tutorials, live streams. Combine with Media Session API for custom controls in PiP window.
Warning: Requires user interaction to trigger (security). Some browsers require video to be playing. Can be disabled by browser or user preferences. Always check document.pictureInPictureEnabled. Handle enter/leave events for UI updates. Only one PiP window at a time.

Notification and Messaging Best Practices

  • Always request notification permission from user gesture - explain why first
  • Use notification tag to replace/update instead of spamming
  • Push notifications require Service Worker and VAPID keys
  • Handle pushsubscriptionchange - subscriptions can expire
  • Badge API only works for installed PWAs - check support
  • Clear app badge when user views content - don't leave stale counts
  • Wake Lock API requires visible page - reacquire after visibility change
  • Release wake lock when done - respects user's battery
  • Idle Detection API is experimental - provide fallback detection
  • Use idle detection for auto-logout, pause sync, save battery
  • PiP requires user interaction to trigger - can't auto-enable on page load
  • Combine PiP with Media Session API for rich controls
  • Handle all notification/PiP events for proper UI state management
  • Test permission flows - handle grant, deny, and "ask later" states