Skip to content

Commit 44168a4

Browse files
committed
Upgrade extension with advanced PiP features: Keyboard shortcuts, Context Menu, Multi-video support, and Document PiP
1 parent 89d5082 commit 44168a4

5 files changed

Lines changed: 276 additions & 24 deletions

File tree

background.js

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
chrome.runtime.onInstalled.addListener(() => {
2+
chrome.contextMenus.create({
3+
id: "toggle-pip-menu",
4+
title: "Toggle Picture-in-Picture",
5+
contexts: ["video", "page"]
6+
});
7+
});
8+
9+
chrome.contextMenus.onClicked.addListener((info, tab) => {
10+
if (info.menuItemId === "toggle-pip-menu") {
11+
triggerPiP(tab.id);
12+
}
13+
});
14+
15+
chrome.commands.onCommand.addListener((command) => {
16+
if (command === "toggle-pip") {
17+
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
18+
const tab = tabs[0];
19+
if (!tab) return;
20+
triggerPiP(tab.id);
21+
});
22+
}
23+
});
24+
25+
function triggerPiP(tabId) {
26+
chrome.scripting.executeScript({
27+
target: { tabId: tabId },
28+
func: async () => {
29+
const video = document.querySelector("video");
30+
if (!video) {
31+
alert("No video found on this page");
32+
return;
33+
}
34+
35+
// Try Document PiP first (Phase 3)
36+
if (typeof requestDocumentPiP === 'function') {
37+
const success = await requestDocumentPiP(video);
38+
if (success) return;
39+
}
40+
41+
// Fallback to native PiP
42+
try {
43+
if (document.pictureInPictureElement) {
44+
await document.exitPictureInPicture();
45+
} else {
46+
await video.requestPictureInPicture();
47+
}
48+
} catch (e) {
49+
console.error("PiP failed:", e);
50+
}
51+
}
52+
});
53+
}
54+

content.js

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,35 @@
1+
// Toast UI Helper
2+
function showToast(message) {
3+
let toast = document.getElementById("pip-toast");
4+
if (!toast) {
5+
toast = document.createElement("div");
6+
toast.id = "pip-toast";
7+
toast.style.cssText = `
8+
position: fixed;
9+
bottom: 20px;
10+
left: 50%;
11+
transform: translateX(-50%);
12+
background: rgba(0, 0, 0, 0.8);
13+
color: white;
14+
padding: 8px 16px;
15+
border-radius: 20px;
16+
z-index: 999999;
17+
font-family: sans-serif;
18+
font-size: 14px;
19+
pointer-events: none;
20+
transition: opacity 0.3s;
21+
`;
22+
document.body.appendChild(toast);
23+
}
24+
toast.textContent = message;
25+
toast.style.opacity = "1";
26+
27+
if (window.toastTimeout) clearTimeout(window.toastTimeout);
28+
window.toastTimeout = setTimeout(() => {
29+
toast.style.opacity = "0";
30+
}, 2000);
31+
}
32+
133
function enablePiP(video) {
234
if (video.dataset.pipEnabled) return;
335
video.dataset.pipEnabled = "true";
@@ -15,9 +47,123 @@ function enablePiP(video) {
1547
}
1648
});
1749

50+
// PiP Event Hooks
51+
video.addEventListener("enterpictureinpicture", () => {
52+
showToast("PiP Mode Enabled 📺");
53+
});
54+
55+
video.addEventListener("leavepictureinpicture", () => {
56+
showToast("PiP Mode Closed");
57+
});
58+
59+
// Document PiP Support (Advanced)
60+
video.dataset.docPipSupported = 'documentPictureInPicture' in window;
61+
}
62+
63+
async function requestDocumentPiP(video) {
64+
if (!('documentPictureInPicture' in window)) return false;
65+
66+
try {
67+
const pipWindow = await window.documentPictureInPicture.requestWindow({
68+
width: video.videoWidth || 640,
69+
height: video.videoHeight || 360,
70+
});
71+
72+
// Style the PiP window
73+
pipWindow.document.body.style.margin = "0";
74+
pipWindow.document.body.style.background = "black";
75+
pipWindow.document.body.style.display = "flex";
76+
pipWindow.document.body.style.alignItems = "center";
77+
pipWindow.document.body.style.justifyContent = "center";
78+
pipWindow.document.body.style.overflow = "hidden";
79+
80+
// Create a container for video and custom controls
81+
const container = document.createElement("div");
82+
container.style.position = "relative";
83+
container.style.width = "100%";
84+
container.style.height = "100%";
85+
86+
// Move video to PiP
87+
const originalParent = video.parentElement;
88+
const originalNextSibling = video.nextSibling;
89+
container.appendChild(video);
90+
pipWindow.document.body.appendChild(container);
1891

92+
// Custom Overlay for Controls (Simplified)
93+
const overlay = document.createElement("div");
94+
overlay.style.cssText = "position:absolute;bottom:10px;left:50%;transform:translateX(-50%);display:none;gap:15px;background:rgba(0,0,0,0.6);padding:10px 20px;border-radius:30px;transition:opacity 0.2s;backdrop-filter:blur(5px);";
95+
overlay.innerHTML = `
96+
<button id="rewind" style="background:none;border:none;color:white;cursor:pointer;font-size:20px;">⏪</button>
97+
<button id="playPause" style="background:none;border:none;color:white;cursor:pointer;font-size:24px;">${video.paused ? '▶️' : '⏸️'}</button>
98+
<button id="forward" style="background:none;border:none;color:white;cursor:pointer;font-size:20px;">⏩</button>
99+
`;
100+
container.appendChild(overlay);
101+
102+
container.onmouseenter = () => overlay.style.display = "flex";
103+
container.onmouseleave = () => overlay.style.display = "none";
104+
105+
// Logic for custom buttons
106+
overlay.querySelector("#playPause").onclick = () => {
107+
if (video.paused) video.play(); else video.pause();
108+
overlay.querySelector("#playPause").textContent = video.paused ? '▶️' : '⏸️';
109+
};
110+
overlay.querySelector("#rewind").onclick = () => video.currentTime -= 5;
111+
overlay.querySelector("#forward").onclick = () => video.currentTime += 5;
112+
113+
// Restore video on close
114+
pipWindow.addEventListener("pagehide", () => {
115+
if (originalNextSibling) {
116+
originalParent.insertBefore(video, originalNextSibling);
117+
} else {
118+
originalParent.appendChild(video);
119+
}
120+
}, { once: true });
121+
122+
return true;
123+
} catch (e) {
124+
console.error("Document PiP failed:", e);
125+
return false;
126+
}
19127
}
20128

129+
130+
// Global Keyboard Shortcuts
131+
window.addEventListener("keydown", (e) => {
132+
// Only trigger if a video exists on the page
133+
const video = document.querySelector("video");
134+
if (!video) return;
135+
136+
// Ignore if typing in an input/textarea
137+
if (["INPUT", "TEXTAREA"].includes(document.activeElement.tagName) || document.activeElement.isContentEditable) {
138+
return;
139+
}
140+
141+
switch (e.key.toLowerCase()) {
142+
case "m":
143+
video.muted = !video.muted;
144+
showToast(video.muted ? "Muted 🔇" : "Unmuted 🔊");
145+
break;
146+
case " ":
147+
e.preventDefault(); // Prevent scroll
148+
if (video.paused) {
149+
video.play();
150+
showToast("Playing ▶️");
151+
} else {
152+
video.pause();
153+
showToast("Paused ⏸️");
154+
}
155+
break;
156+
case "arrowleft":
157+
video.currentTime = Math.max(0, video.currentTime - 5);
158+
showToast("Rewind 5s ⏪");
159+
break;
160+
case "arrowright":
161+
video.currentTime = Math.min(video.duration, video.currentTime + 5);
162+
showToast("Forward 5s ⏩");
163+
break;
164+
}
165+
});
166+
21167
// Detect existing videos
22168
document.querySelectorAll("video").forEach(enablePiP);
23169

manifest.json

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,26 @@
55
"description": "Mini player and Picture-in-Picture for any website",
66
"permissions": [
77
"scripting",
8-
"activeTab"
8+
"activeTab",
9+
"contextMenus"
910
],
1011
"host_permissions": [
1112
"<all_urls>"
1213
],
1314
"action": {
1415
"default_popup": "popup.html"
1516
},
17+
"background": {
18+
"service_worker": "background.js"
19+
},
20+
"commands": {
21+
"toggle-pip": {
22+
"suggested_key": {
23+
"default": "Alt+P"
24+
},
25+
"description": "Toggle Picture-in-Picture"
26+
}
27+
},
1628
"content_scripts": [
1729
{
1830
"matches": [

popup.html

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
<!DOCTYPE html>
22
<html>
33

4-
<body style="font-family:sans-serif;padding:10px;width:200px;background:#0f0f12;color:white;">
4+
<body
5+
style="font-family:'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;padding:15px;width:220px;background:#0f0f12;color:white;margin:0;">
6+
<h3 style="margin-top:0;font-size:16px;color:#3b82f6;">PiP Controller</h3>
57
<button id="pipBtn"
6-
style="width:100%;padding:10px;cursor:pointer;background:#3b82f6;color:white;border:none;border-radius:8px;font-weight:bold;">
7-
Toggle Picture-in-Picture
8+
style="width:100%;padding:10px;cursor:pointer;background:#3b82f6;color:white;border:none;border-radius:8px;font-weight:bold;margin-bottom:10px;transition:background 0.2s;">
9+
Toggle PiP
810
</button>
11+
<div id="videoListContainer" style="display:none;">
12+
<p style="font-size:12px;color:#aaa;margin-bottom:8px;">Multiple videos detected:</p>
13+
<div id="videoList" style="display:flex;flex-direction:column;gap:5px;"></div>
14+
</div>
915
<script src="popup.js"></script>
1016
</body>
1117

popup.js

Lines changed: 54 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,61 @@
1-
document.getElementById("pipBtn").addEventListener("click", async () => {
2-
const [tab] = await chrome.tabs.query({
3-
active: true,
4-
currentWindow: true
5-
});
1+
const pipBtn = document.getElementById("pipBtn");
2+
const videoListContainer = document.getElementById("videoListContainer");
3+
const videoList = document.getElementById("videoList");
4+
5+
async function init() {
6+
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
67

8+
// Detect videos
79
chrome.scripting.executeScript({
810
target: { tabId: tab.id },
9-
func: async () => {
10-
const video = document.querySelector("video");
11-
if (!video) {
12-
alert("No video found on this page");
13-
return;
14-
}
11+
func: () => {
12+
const videos = Array.from(document.querySelectorAll("video"));
13+
return videos.map((v, i) => ({
14+
id: i,
15+
currentSrc: v.currentSrc || "Unnamed Video",
16+
playing: !v.paused
17+
}));
18+
}
19+
}, (results) => {
20+
if (!results || !results[0].result) return;
21+
const videos = results[0].result;
1522

16-
try {
17-
if (document.pictureInPictureElement) {
18-
await document.exitPictureInPicture();
19-
} else {
20-
await video.requestPictureInPicture();
21-
}
22-
} catch (e) {
23-
console.error("PiP failed:", e);
24-
}
23+
if (videos.length > 1) {
24+
videoListContainer.style.display = "block";
25+
videoList.innerHTML = "";
26+
videos.forEach(v => {
27+
const btn = document.createElement("button");
28+
btn.style.cssText = "width:100%;padding:8px;background:#27272a;color:white;border:none;border-radius:6px;font-size:11px;text-align:left;cursor:pointer;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;";
29+
btn.textContent = `Video ${v.id + 1}: ${v.currentSrc.split('/').pop() || 'Blob/Stream'}`;
30+
btn.onclick = () => triggerPiPForIndex(tab.id, v.id);
31+
videoList.appendChild(btn);
32+
});
2533
}
2634
});
35+
}
36+
37+
pipBtn.addEventListener("click", async () => {
38+
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
39+
triggerPiPForIndex(tab.id, 0); // Default to first video
2740
});
41+
42+
function triggerPiPForIndex(tabId, index) {
43+
chrome.scripting.executeScript({
44+
target: { tabId: tabId },
45+
func: (idx) => {
46+
const videos = document.querySelectorAll("video");
47+
const video = videos[idx];
48+
if (!video) return;
49+
50+
if (document.pictureInPictureElement) {
51+
document.exitPictureInPicture();
52+
} else {
53+
video.requestPictureInPicture().catch(console.error);
54+
}
55+
},
56+
args: [index]
57+
});
58+
}
59+
60+
init();
61+

0 commit comments

Comments
 (0)