From 140b57aea19764264b82dd3cab3f0f4c197229a3 Mon Sep 17 00:00:00 2001 From: sneha kumari Date: Thu, 6 Mar 2025 21:21:05 +0530 Subject: [PATCH] readme --- README.md | 77 ++++++++++++ camera.html | 74 ++++++++---- camera.js | 117 +++++++++++------- offscreen.js | 335 +++++++++++++++++++++++++-------------------------- popup.html | 133 ++++++++++++++++---- popup.js | 107 +++++++++++----- 6 files changed, 553 insertions(+), 290 deletions(-) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..3d85844 --- /dev/null +++ b/README.md @@ -0,0 +1,77 @@ +# πŸš€ Bubblecam - Your Webcam, Your Way! + +## πŸŽ₯ What's Bubblecam? +Bubblecam lets you control your screen recoding like a boss. Use your **real camera** or switch to a **custom avatar**, drag it anywhere on your screen, and even **record your screen**! Works like magic in Microsoft Edge. 😎 + +--- + +## πŸ”₯ Key Features +βœ… **Toggle Between Camera & Avatar** - Be on cam or flex your avatar, your choice! +βœ… **Custom Avatar Upload** - Upload your own avatar to replace your webcam feed. +βœ… **Draggable Camera Bubble** - Move it **anywhere** on the screen, no restrictions! +βœ… **Screen Recording** - Capture your screen directly from the pop-up. +βœ… **Easy to Use** - No complicated setup, just load & go! + +--- + +## πŸ› οΈ Tech Stack +- **HTML** (for the UI) +- **JavaScript** (for all the cool interactivity) + +--- + +## πŸ“‚ Project Structure +``` +bubblecam/ +β”‚ +β”œβ”€β”€ icons/ # All the cool icons +β”‚ +β”œβ”€β”€ camera.html # Main camera interface +β”œβ”€β”€ camera.js # Handles camera logic +β”œβ”€β”€ content.js # Manages draggable camera bubble +β”œβ”€β”€ desktopRecord.html # UI for screen recording +β”œβ”€β”€ desktopRecord.js # Logic for screen recording +β”œβ”€β”€ offscreen.html # Off-screen camera handling +β”œβ”€β”€ offscreen.js # Off-screen magic happens here +β”œβ”€β”€ popup.html # Pop-up UI +β”œβ”€β”€ popup.js # Controls pop-up window +β”œβ”€β”€ service-worker.js # Injects & removes camera based on activity +``` + +--- + +## πŸ—οΈ How to Install & Use in Microsoft Edge (100% Free!) +### **1️⃣ Download & Extract the ZIP** +- Hit **Download ZIP** (if it's from GitHub, click the green `<> Code` button > `Download ZIP`). +- Extract the folder to your PC. + +### **2️⃣ Load It as an Unpacked Extension in Edge** +1. Open **Microsoft Edge** (yep, only Edge, sorry Chrome gang 🫠). +2. Go to `edge://extensions/`. +3. Toggle **Developer Mode** (top-right corner). +4. Click **"Load unpacked"**. +5. Select the **bubblecam** folder (the one you extracted). +6. Boom! Bubblecam is installed. πŸŽ‰ + +### **3️⃣ How to Use Bubblecam** +1. Click on the **Bubblecam icon** in Edge’s toolbar. +2. Choose **Camera Mode** or **Avatar Mode**. +3. **Upload Your Avatar** (if using avatar mode). +4. Drag your camera bubble anywhere you want! πŸ€ +5. Click **Record** to start screen recording. +6. Have fun being the director of your own screen. 🎬 + +--- + +## ⚑ Prerequisites +- A PC with **Microsoft Edge** installed. +- Camera & microphone permissions enabled (duh, you need a cam!). + +--- + +## πŸ“œ License +Licensed under the **MIT License** – basically, you're free to use it however you want! Just don’t sue me if your avatar starts vibing too hard. πŸ˜† + +--- + + diff --git a/camera.html b/camera.html index b88d535..20a2283 100644 --- a/camera.html +++ b/camera.html @@ -1,29 +1,59 @@ - - - - - Document - + + + Camera + - -
- +
+ - \ No newline at end of file diff --git a/camera.js b/camera.js index 857d230..9078d6b 100644 --- a/camera.js +++ b/camera.js @@ -6,60 +6,66 @@ const runCode = async () => { const cameraElement = document.querySelector("#camera"); const startCamera = async () => { - // Check permissions first - const permissions = await navigator.permissions.query({ - name: "camera", - }); - - if (permissions.state === "prompt") { - await navigator.mediaDevices.getUserMedia({ audio: true, video: true }); - return; - } - - if (permissions.state === "denied") { - alert("Camera permissions denied"); - return; - } - - // Only proceed with camera if we're in camera mode - if (currentMode === 'avatar') { - displayAvatar(); - return; - } - - const videoElement = document.createElement("video"); - videoElement.setAttribute("id", "cam"); - videoElement.setAttribute( - "style", - ` - height: 200px; - border-radius: 100px; - transform: scaleX(-1); - ` - ); - videoElement.setAttribute("autoplay", true); - videoElement.setAttribute("muted", true); try { + const permissions = await navigator.permissions.query({ + name: "camera", + }); + + if (permissions.state === "prompt") { + await navigator.mediaDevices.getUserMedia({ audio: true, video: true }); + return; + } + + if (permissions.state === "denied") { + console.log("Camera permissions denied, falling back to avatar"); + currentMode = 'avatar'; + displayAvatar(); + return; + } + + + if (currentMode === 'avatar') { + displayAvatar(); + return; + } + + const videoElement = document.createElement("video"); + videoElement.setAttribute("id", "cam"); + videoElement.setAttribute( + "style", + ` + height: 200px; + width: 200px; + border-radius: 100px; + transform: scaleX(-1); + object-fit: cover; + ` + ); + videoElement.setAttribute("autoplay", true); + videoElement.setAttribute("muted", true); + cameraStream = await navigator.mediaDevices.getUserMedia({ audio: false, - video: true, + video: { + width: { ideal: 400 }, + height: { ideal: 400 }, + facingMode: "user" + } }); videoElement.srcObject = cameraStream; - // Clear any existing content cameraElement.innerHTML = ''; cameraElement.appendChild(videoElement); } catch (err) { console.error("Error accessing camera:", err); - // Fallback to avatar if camera fails + currentMode = 'avatar'; displayAvatar(); } }; const displayAvatar = () => { - // Stop any existing camera stream if (cameraStream) { stopCamera(); } @@ -73,15 +79,42 @@ const runCode = async () => { width: 200px; border-radius: 100px; object-fit: cover; + background-color: #5d3fbd; ` ); - avatarImg.src = avatarData || 'default-avatar.png'; - // Clear any existing content + + if (avatarData) { + avatarImg.src = avatarData; + } else { + + avatarImg.src = 'avatar.png'; + avatarImg.onerror = () => { + + avatarImg.style.backgroundColor = '#5d3fbd'; + avatarImg.style.display = 'flex'; + avatarImg.style.justifyContent = 'center'; + avatarImg.style.alignItems = 'center'; + avatarImg.src = ''; + + // Create a text element for initials + const initialsElem = document.createElement('div'); + initialsElem.textContent = 'BC'; + initialsElem.style.color = 'white'; + initialsElem.style.fontSize = '60px'; + initialsElem.style.fontWeight = 'bold'; + + cameraElement.innerHTML = ''; + cameraElement.appendChild(initialsElem); + }; + } + + cameraElement.innerHTML = ''; cameraElement.appendChild(avatarImg); }; + const stopCamera = () => { if (cameraStream) { cameraStream.getTracks().forEach(track => { @@ -101,7 +134,6 @@ const runCode = async () => { } }; - // Message listeners for camera/avatar control chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message.type === "stop-camera") { stopCamera(); @@ -125,7 +157,7 @@ const runCode = async () => { } }); - // Initialize based on saved mode + chrome.storage.local.get(['mode', 'avatarData'], function(data) { currentMode = data.mode || 'camera'; avatarData = data.avatarData; @@ -138,4 +170,5 @@ const runCode = async () => { }); }; -runCode(); \ No newline at end of file + +document.addEventListener('DOMContentLoaded', runCode); \ No newline at end of file diff --git a/offscreen.js b/offscreen.js index 8b5d092..a25347b 100644 --- a/offscreen.js +++ b/offscreen.js @@ -1,182 +1,173 @@ //offscreen.js chrome.runtime.onMessage.addListener(async (message, sender, sendResponse) => { - console.log("[offscreen] message received", message, sender); - - switch (message.type) { - case "start-recording": - console.log("start recording received in offscreen.js"); - - await startRecording(message.data, message.quality); - sendResponse({ status: "recording-started" }); + console.log("[offscreen] message received", message, sender); + + switch (message.type) { + case "start-recording": + console.log("start recording received in offscreen.js"); + + await startRecording(message.data, message.quality); + sendResponse({ status: "recording-started" }); + break; + case "stop-recording": + console.log("stop recording received in offscreen.js"); + + await stopRecording(); + sendResponse({ status: "recording-stopped" }); + break; + default: + console.log("default"); + sendResponse({ status: "unknown-message" }); + } + + return true; +}); + +let recorder; +let data = []; + +async function stopRecording() { + console.log("Entered stopRecording"); + + if (recorder?.state === "recording") { + console.log("Recorder state is 'recording', stopping..."); + recorder.stop(); + + // Send a message to the content script to stop the camera + chrome.runtime.sendMessage({ type: "stop-camera" }, response => { + if (response?.status === "camera-stopped") { + console.log("Camera has been successfully stopped."); + } else { + console.log("Failed to stop the camera."); + } + }); + } else { + console.log("No active recording found or recorder is not in 'recording' state."); + } + + console.log("Stopped the recording"); +} + +function stopAllMediaStreams(media, microphone) { + media.getTracks().forEach((track) => { + track.stop(); + console.log("Media Track stopped:", track); + }); + + microphone.getTracks().forEach((track) => { + track.stop(); + console.log("Microphone Track stopped", track); + }); +} + +async function startRecording(streamId, quality) { + try { + if (recorder?.state === "recording") { + throw new Error("Called startRecording while recording is in progress."); + } + + console.log("start recording", streamId); + console.log("qaulity inside offfscreen.js", quality); + + let videoConstraints; + + switch (quality) { + case "low": + videoConstraints = { + mandatory: { + chromeMediaSource: "tab", + chromeMediaSourceId: streamId, + maxWidth: 640, + maxHeight: 480, + minWidth: 640, + minHeight: 480, + maxFrameRate: 15, + }, + }; break; - case "stop-recording": - console.log("stop recording received in offscreen.js"); - - await stopRecording(); - sendResponse({ status: "recording-stopped" }); + case "medium": + videoConstraints = { + mandatory: { + chromeMediaSource: "tab", + chromeMediaSourceId: streamId, + maxWidth: 1280, + maxHeight: 720, + minWidth: 1280, + minHeight: 720, + maxFrameRate: 30, + }, + }; + break; + case "high": + videoConstraints = { + mandatory: { + chromeMediaSource: "tab", + chromeMediaSourceId: streamId, + maxWidth: 1920, + maxHeight: 1080, + minWidth: 1920, + minHeight: 1080, + maxFrameRate: 60, + }, + }; break; default: - console.log("default"); - sendResponse({ status: "unknown-message" }); - } - - return true; - }); - - - let recorder; - let data = []; - - async function stopRecording() { - console.log("Entered stopRecording"); - - if (recorder?.state === "recording") { - console.log("Recorder state is 'recording', stopping..."); - recorder.stop(); - - // Send a message to the content script to stop the camera - chrome.runtime.sendMessage({ type: "stop-camera" }, response => { - if (response?.status === "camera-stopped") { - console.log("Camera has been successfully stopped."); - } else { - console.log("Failed to stop the camera."); - } - }); - } else { - console.log("No active recording found or recorder is not in 'recording' state."); - } - - console.log("Stopped the recording"); - } - - function stopAllMediaStreams(media, microphone) { - media.getTracks().forEach((track) => { - track.stop(); - console.log("Media Track stopped:", track); - }); - - microphone.getTracks().forEach((track) => { - track.stop(); - console.log("Microphone Track stopped", track); - }); - } - - async function startRecording(streamId, quality) { - try { - if (recorder?.state === "recording") { - throw new Error("Called startRecording while recording is in progress."); - } - - console.log("start recording", streamId); - console.log("qaulity inside offfscreen.js", quality); - - - let videoConstraints; - - switch (quality) { - case "low": - videoConstraints = { - mandatory: { - chromeMediaSource: "tab", - chromeMediaSourceId: streamId, - maxWidth: 640, - maxHeight: 480, - minWidth: 640, - minHeight: 480, - maxFrameRate: 15, - }, - }; - break; - case "medium": - videoConstraints = { - mandatory: { - chromeMediaSource: "tab", - chromeMediaSourceId: streamId, - maxWidth: 1280, - maxHeight: 720, - minWidth: 1280, - minHeight: 720, - maxFrameRate: 30, - }, - }; - break; - case "high": - videoConstraints = { - mandatory: { - chromeMediaSource: "tab", - chromeMediaSourceId: streamId, - maxWidth: 1920, - maxHeight: 1080, - minWidth: 1920, - minHeight: 1080, - maxFrameRate: 60, - }, - }; - break; - default: - videoConstraints = { - mandatory: { - chromeMediaSource: "tab", - chromeMediaSourceId: streamId, - }, - }; - } - - - - const media = await navigator.mediaDevices.getUserMedia({ - audio: { + videoConstraints = { mandatory: { chromeMediaSource: "tab", chromeMediaSourceId: streamId, }, - }, - video: videoConstraints - }); - - - const microphone = await navigator.mediaDevices.getUserMedia({ - audio: { echoCancellation: false }, - }); - - const mixedContext = new AudioContext(); - const mixedDest = mixedContext.createMediaStreamDestination(); - - mixedContext.createMediaStreamSource(microphone).connect(mixedDest); - mixedContext.createMediaStreamSource(media).connect(mixedDest); - - const combinedStream = new MediaStream([ - media.getVideoTracks()[0], - mixedDest.stream.getTracks()[0], - ]); - - recorder = new MediaRecorder(combinedStream, { mimeType: "video/webm" }); - - - recorder.ondataavailable = (event) => { - console.log("data available", event); - data.push(event.data); - }; - - - recorder.onstop = async () => { - console.log("recording stopped"); - // send the data to the service worker - console.log("sending data to service worker"); - stopAllMediaStreams(media, microphone); - - recorder = null; - - - const blob = new Blob(data, { type: "video/webm" }); - const url = URL.createObjectURL(blob); - - chrome.runtime.sendMessage({ type: "open-tab", url }); - }; - - - recorder.start(); - } catch (err) { - console.log("error", err); + }; } - } \ No newline at end of file + + const media = await navigator.mediaDevices.getUserMedia({ + audio: { + mandatory: { + chromeMediaSource: "tab", + chromeMediaSourceId: streamId, + }, + }, + video: videoConstraints + }); + + const microphone = await navigator.mediaDevices.getUserMedia({ + audio: { echoCancellation: false }, + }); + + const mixedContext = new AudioContext(); + const mixedDest = mixedContext.createMediaStreamDestination(); + + mixedContext.createMediaStreamSource(microphone).connect(mixedDest); + mixedContext.createMediaStreamSource(media).connect(mixedDest); + + const combinedStream = new MediaStream([ + media.getVideoTracks()[0], + mixedDest.stream.getTracks()[0], + ]); + + recorder = new MediaRecorder(combinedStream, { mimeType: "video/webm" }); + + recorder.ondataavailable = (event) => { + console.log("data available", event); + data.push(event.data); + }; + + recorder.onstop = async () => { + console.log("recording stopped"); + // send the data to the service worker + console.log("sending data to service worker"); + stopAllMediaStreams(media, microphone); + + recorder = null; + + const blob = new Blob(data, { type: "video/webm" }); + const url = URL.createObjectURL(blob); + + chrome.runtime.sendMessage({ type: "open-tab", url }); + }; + + recorder.start(); + } catch (err) { + console.log("error", err); + } +} \ No newline at end of file diff --git a/popup.html b/popup.html index 988cc55..fcd1697 100644 --- a/popup.html +++ b/popup.html @@ -16,7 +16,7 @@
-

Recorder

+

BUBBLE CAM

@@ -124,44 +204,51 @@
- Avatar + Avatar
- - + +
-
-
-
-
-
-
-
- + + +
+ \ No newline at end of file diff --git a/popup.js b/popup.js index 3640bf0..075b1e2 100644 --- a/popup.js +++ b/popup.js @@ -1,12 +1,10 @@ -//popup.js document.addEventListener('DOMContentLoaded', function() { const cameraMode = document.getElementById('camera-mode'); const avatarMode = document.getElementById('avatar-mode'); const avatarSection = document.getElementById('avatar-section'); const avatarInput = document.getElementById('avatar-input'); const avatarPreview = document.getElementById('avatar-img'); - - // Mode switching + cameraMode.addEventListener('click', () => { cameraMode.classList.add('active'); avatarMode.classList.remove('active'); @@ -23,31 +21,86 @@ document.addEventListener('DOMContentLoaded', function() { chrome.runtime.sendMessage({ type: 'mode-change', mode: 'avatar' }); }); - // Avatar upload handling + // Fix the avatar upload functionality avatarInput.addEventListener('change', function(e) { - const file = e.target.files[0]; - if (file) { - const reader = new FileReader(); - reader.onload = function(e) { - const avatarData = e.target.result; - avatarPreview.src = avatarData; - chrome.storage.local.set({ avatarData: avatarData }); + console.log("File input changed"); // Add this debug line + const file = e.target.files[0]; + if (!file) { + console.log("No file selected"); + return; + } + + console.log("File selected:", file.name); // Add this debug line + + if (!file.type.match('image.*')) { + alert('Please select an image file'); + return; + } + + if (file.size > 2 * 1024 * 1024) { + alert('Image is too large. Please select an image under 2MB.'); + return; + } + + const reader = new FileReader(); + reader.onload = function(e) { + try { + const avatarData = e.target.result; + console.log("File loaded successfully"); // Add this debug line + + avatarPreview.src = avatarData; + avatarPreview.style.display = 'block'; + + // Store the avatar data in Chrome storage + chrome.storage.local.set({ avatarData: avatarData }, function() { + if (chrome.runtime.lastError) { + console.error('Error saving avatar:', chrome.runtime.lastError); + return; + } + + console.log("Avatar saved to storage"); // Add this debug line + + // Notify any active content scripts about the avatar change + chrome.tabs.query({active: true, currentWindow: true}, function(tabs) { + if (tabs[0]?.id) { chrome.runtime.sendMessage({ - type: 'avatar-update', - avatarData: avatarData + type: 'avatar-update', + avatarData: avatarData }); - }; - reader.readAsDataURL(file); + } + }); + }); + } catch (error) { + console.error('Error processing avatar:', error); + alert('There was an error processing your image. Please try another.'); } + }; + + reader.onerror = function() { + console.error('Error reading file'); + alert('Error reading the image file. Please try again.'); + }; + + reader.readAsDataURL(file); }); - // Load saved mode and avatar + + chrome.storage.local.get(['mode', 'avatarData'], function(data) { + if (data.mode === 'avatar') { avatarMode.click(); - if (data.avatarData) { - avatarPreview.src = data.avatarData; - } + } else { + cameraMode.click(); + } + + if (data.avatarData) { + avatarPreview.src = data.avatarData; + avatarPreview.style.display = 'block'; + } else { + + avatarPreview.src = 'avatar.png'; + avatarPreview.style.display = 'block'; } }); }); @@ -68,11 +121,10 @@ const injectCamera = async () => { }); }; -// Initialize camera bubble when popup opens + injectCamera(); const removeCamera = async () => { - const tab = await chrome.tabs.query({ active: true, currentWindow: true }); if (!tab) return; @@ -80,18 +132,13 @@ const removeCamera = async () => { console.log("inject into tab", tabId); await chrome.scripting.executeScript({ func: () => { - const camera = document.querySelector("#purple-camera"); - if (!camera) return; - camera.remove(); - document.querySelector("#purple-camera").style.display = "none"; + const camera = document.querySelector("#camera-wrapper"); + if (camera) camera.remove(); }, target: { tabId }, }); }; - - - const checkRecording = async () => { const recording = await chrome.storage.local.get(["recording", "type"]); const recordingStatus = recording.recording || false; @@ -130,7 +177,6 @@ const init = async () => { const recordingState = await checkRecording(); if (recordingState[0] === true) { - chrome.runtime.sendMessage({ type: "stop-recording" }); removeCamera(); } else { @@ -142,7 +188,6 @@ const init = async () => { injectCamera(); } - setTimeout(() => { window.close(); }, 100); @@ -159,4 +204,4 @@ const init = async () => { }); }; -init(); +init(); \ No newline at end of file