bubble cam

This commit is contained in:
sneha kumari 2025-02-16 15:10:07 +05:30
commit 9a1171bb35
17 changed files with 2452 additions and 0 deletions

29
camera.html Normal file
View File

@ -0,0 +1,29 @@
<!-- //camera.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
body {
background-color: rgb(168, 29, 203);
margin: 0;
padding: 0;
}
div#camera {
width: 200px;
height: 200px;
overflow: hidden;
}
</style>
</head>
<body>
<div id="camera"></div>
<script src="camera.js"></script>
</body>
</html>

141
camera.js Normal file
View File

@ -0,0 +1,141 @@
let cameraStream = null;
let currentMode = 'camera';
let avatarData = null;
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 {
cameraStream = await navigator.mediaDevices.getUserMedia({
audio: false,
video: true,
});
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
displayAvatar();
}
};
const displayAvatar = () => {
// Stop any existing camera stream
if (cameraStream) {
stopCamera();
}
const avatarImg = document.createElement("img");
avatarImg.setAttribute("id", "avatar");
avatarImg.setAttribute(
"style",
`
height: 200px;
width: 200px;
border-radius: 100px;
object-fit: cover;
`
);
avatarImg.src = avatarData || 'default-avatar.png';
// Clear any existing content
cameraElement.innerHTML = '';
cameraElement.appendChild(avatarImg);
};
const stopCamera = () => {
if (cameraStream) {
cameraStream.getTracks().forEach(track => {
track.stop();
console.log("Camera track stopped:", track);
});
cameraStream = null;
const cameraElement = document.getElementById("purple-camera");
if (cameraElement) {
cameraElement.remove();
}
const videoElement = document.getElementById("cam");
if (videoElement) {
videoElement.remove();
}
}
};
// Message listeners for camera/avatar control
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === "stop-camera") {
stopCamera();
sendResponse({ status: "camera-stopped" });
}
else if (message.type === "mode-change") {
currentMode = message.mode;
if (currentMode === 'camera') {
startCamera();
} else {
displayAvatar();
}
sendResponse({ status: "mode-changed" });
}
else if (message.type === "avatar-update") {
avatarData = message.avatarData;
if (currentMode === 'avatar') {
displayAvatar();
}
sendResponse({ status: "avatar-updated" });
}
});
// Initialize based on saved mode
chrome.storage.local.get(['mode', 'avatarData'], function(data) {
currentMode = data.mode || 'camera';
avatarData = data.avatarData;
if (currentMode === 'camera') {
startCamera();
} else {
displayAvatar();
}
});
};
runCode();

88
content.js Normal file
View File

@ -0,0 +1,88 @@
// content.js
window.cameraId = "purple-camera";
function createDraggableCamera() {
const wrapper = document.createElement('div');
wrapper.id = "camera-wrapper";
wrapper.setAttribute('style', `
position: fixed;
width: 200px;
height: 200px;
top: 20px;
right: 20px;
z-index: 999999;
border-radius: 100%;
cursor: move;
user-select: none;
transition: background 0.2s;
&:hover {
background: rgba(0, 0, 0, 0.1);
}
`);
const camera = document.createElement("iframe");
camera.id = cameraId;
camera.setAttribute("style", `
width: 100%;
height: 100%;
border-radius: 100%;
border: 2px solid rgba(168, 29, 203, 0.5);
background: black;
pointer-events: none;
`);
camera.setAttribute("allow", "camera; microphone");
camera.src = chrome.runtime.getURL("camera.html");
wrapper.appendChild(camera);
document.body.appendChild(wrapper);
let isDragging = false;
let currentX;
let currentY;
let initialX;
let initialY;
wrapper.addEventListener('mousedown', e => {
isDragging = true;
wrapper.style.transition = 'none';
initialX = e.clientX - wrapper.offsetLeft;
initialY = e.clientY - wrapper.offsetTop;
});
document.addEventListener('mousemove', e => {
if (!isDragging) return;
e.preventDefault();
currentX = e.clientX - initialX;
currentY = e.clientY - initialY;
// Keep camera within viewport bounds
const maxX = window.innerWidth - wrapper.offsetWidth;
const maxY = window.innerHeight - wrapper.offsetHeight;
currentX = Math.min(Math.max(0, currentX), maxX);
currentY = Math.min(Math.max(0, currentY), maxY);
wrapper.style.left = `${currentX}px`;
wrapper.style.top = `${currentY}px`;
});
document.addEventListener('mouseup', () => {
isDragging = false;
wrapper.style.transition = 'background 0.2s';
});
// Handle window resize
window.addEventListener('resize', () => {
const maxX = window.innerWidth - wrapper.offsetWidth;
const maxY = window.innerHeight - wrapper.offsetHeight;
wrapper.style.left = `${Math.min(parseInt(wrapper.style.left), maxX)}px`;
wrapper.style.top = `${Math.min(parseInt(wrapper.style.top), maxY)}px`;
});
}
// Initialize camera if it doesn't exist
if (!document.getElementById(cameraId)) {
createDraggableCamera();
}

BIN
default-avatar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 KiB

15
desktopRecord.html Normal file
View File

@ -0,0 +1,15 @@
<!-- //desktopRecord.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script src="desktopRecord.js"></script>
</body>
</html>

227
desktopRecord.js Normal file
View File

@ -0,0 +1,227 @@
//desktopRecord.js
const convertBlobToBase64 = (blob) => {
return new Promise((resolve) => {
const reader = new FileReader();
reader.readAsDataURL(blob);
reader.onloadend = () => {
const base64data = reader.result;
resolve(base64data);
};
});
};
const fetchBlob = async (url) => {
const response = await fetch(url);
const blob = await response.blob();
const base64 = await convertBlobToBase64(blob);
return base64;
};
let recorder = null;
let recordingData = null;
chrome.runtime.onMessage.addListener(function (request, sender) {
console.log("message received", request, sender);
switch (request.type) {
case "start-recording":
startRecording(request.focusedTabId, request.quality);
break;
case "stop-recording":
stopRecording();
break;
default:
console.log("default");
}
return true;
});
async function stopRecording() {
console.log("Entered stopRecording");
if (recorder?.state === "recording") {
console.log("Recorder state is 'recording', stopping...");
recorder.stop();
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.");
}
});
chrome.tabs.query({}, (tabs) => {
tabs.forEach((tab) => {
chrome.tabs.sendMessage(tab.id, { type: "stop-camera" }, (response) => {
if (response?.status === "camera-stopped") {
console.log(`Camera stopped on tab ${tab.id}`);
} else {
console.log(`Failed to stop camera on tab ${tab.id}`);
}
});
});
});
} else {
console.log("No active recording found or recorder is not in 'recording' state.");
}
}
function stopAllMediaStreams(stream, microphone) {
if (stream) {
stream.getTracks().forEach((track) => {
track.stop();
console.log("Media Track stopped:", track);
});
}
if (microphone) {
microphone.getTracks().forEach((track) => {
track.stop();
console.log("Microphone Track stopped", track);
});
}
}
const startRecording = async (focusedTabId, quality) => {
console.log("inside desktopRecord.js", quality);
if (recorder) {
await stopRecording();
// Wait for the previous recording to fully stop
await new Promise(resolve => setTimeout(resolve, 100));
}
// Reset recording data
recordingData = [];
chrome.desktopCapture.chooseDesktopMedia(
["screen", "window"],
async function (streamId) {
if (streamId === null) {
return;
}
console.log("stream id from desktop capture", streamId);
let videoConstraints;
switch (quality) {
case "low":
videoConstraints = {
mandatory: {
chromeMediaSource: "desktop",
chromeMediaSourceId: streamId,
maxWidth: 640,
maxHeight: 480,
minWidth: 640,
minHeight: 480,
maxFrameRate: 15,
},
};
break;
case "medium":
videoConstraints = {
mandatory: {
chromeMediaSource: "desktop",
chromeMediaSourceId: streamId,
maxWidth: 1280,
maxHeight: 720,
minWidth: 1280,
minHeight: 720,
maxFrameRate: 30,
},
};
break;
case "high":
videoConstraints = {
mandatory: {
chromeMediaSource: "desktop",
chromeMediaSourceId: streamId,
maxWidth: 1920,
maxHeight: 1080,
minWidth: 1920,
minHeight: 1080,
maxFrameRate: 60,
},
};
break;
default:
videoConstraints = {
mandatory: {
chromeMediaSource: "desktop",
chromeMediaSourceId: streamId,
},
};
}
try {
const stream = await navigator.mediaDevices.getUserMedia({
audio: {
mandatory: {
chromeMediaSource: "desktop",
chromeMediaSourceId: streamId,
},
},
video: videoConstraints
});
console.log("stream from desktop capture", stream);
const microphone = await navigator.mediaDevices.getUserMedia({
audio: { echoCancellation: false },
});
if (microphone.getAudioTracks().length !== 0) {
const combinedStream = new MediaStream([
stream.getVideoTracks()[0],
microphone.getAudioTracks()[0],
]);
console.log("combined stream", combinedStream);
recorder = new MediaRecorder(combinedStream, {
mimeType: "video/webm",
});
recorder.ondataavailable = (event) => {
console.log("data available", event);
if (event.data.size > 0) {
recordingData.push(event.data);
}
};
recorder.onstop = async () => {
console.log("recording stopped");
stopAllMediaStreams(stream, microphone);
const currentData = recordingData;
recorder = null;
recordingData = null;
if (currentData && currentData.length > 0) {
const blobFile = new Blob(currentData, { type: "video/webm" });
const base64 = await fetchBlob(URL.createObjectURL(blobFile));
console.log("send message to open tab", base64);
chrome.runtime.sendMessage({ type: "open-tab", base64 });
}
};
recorder.start();
if (focusedTabId) {
chrome.tabs.update(focusedTabId, { active: true });
}
}
} catch (error) {
console.error("Error starting recording:", error);
stopAllMediaStreams(null, null);
recorder = null;
recordingData = null;
}
}
);
};

BIN
icons/not-recording.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

BIN
icons/recording.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

63
manifest.json Normal file
View File

@ -0,0 +1,63 @@
{
"name": "BUBBLE CAM",
"description": "Records the current tab in an offscreen document & the whole screen",
"version": "1",
"manifest_version": 3,
"minimum_chrome_version": "116",
"action": {
"default_icon": {
"16": "icons/not-recording.png",
"32": "icons/not-recording.png"
},
"default_popup": "popup.html"
},
"host_permissions": [
"<all_urls>",
"https://www.googleapis.com/*",
"https://oauth2.googleapis.com/*",
"https://accounts.google.com/*",
"https://api.vimeo.com/*",
"https://asia-files.tus.vimeo.com/*"
],
"background": {
"service_worker": "service-worker.js"
},
"permissions": [
"tabCapture",
"offscreen",
"scripting",
"storage",
"desktopCapture",
"tabs",
"activeTab",
"downloads",
"identity"
],
"oauth2": {
"client_id": "728787049181-iq4lnrcks0fifee7r6h57h7h71berii6.apps.googleusercontent.com",
"scopes": [
"https://www.googleapis.com/auth/youtube.upload",
"https://www.googleapis.com/auth/userinfo.profile"
]
},
"content_security_policy": {
"extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'"
},
"web_accessible_resources": [
{
"resources": [
"camera.html",
"camera.js",
"video.html",
"video.js",
"*.wasm",
"avatar.png"
],
"matches": [
"https://*/*",
"http://*/*",
"<all_urls>"
]
}
]
}

16
offscreen.html Normal file
View File

@ -0,0 +1,16 @@
<!-- offscreen.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script src="offscreen.js"></script>
</body>
</html>

182
offscreen.js Normal file
View File

@ -0,0 +1,182 @@
//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" });
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 "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: {
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);
}
}

167
popup.html Normal file
View File

@ -0,0 +1,167 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css"
integrity="sha512-Kc323vGBEqzTmouAECnVceyQqyqdsSiqLQISBL29aUW4U/M7pSPA/gEUZQqv1cwx4OnYxTxve5UMg5GT6L4JJg=="
crossorigin="anonymous" referrerpolicy="no-referrer" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link
href="https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap"
rel="stylesheet">
<title>Screen Recorder</title>
<style>
body {
width: 300px;
height: 320px;
font-family: Poppins, sans-serif;
background: #251f38;
--gap: 5em;
--line: 1px;
--color: rgba(255, 255, 255, 0.1);
color: white;
background-image:
linear-gradient(-90deg,
transparent calc(var(--gap) - var(--line)),
var(--color) calc(var(--gap) - var(--line) + 1px),
var(--color) var(--gap)),
linear-gradient(0deg,
transparent calc(var(--gap) - var(--line)),
var(--color) calc(var(--gap) - var(--line) + 1px),
var(--color) var(--gap));
background-size: var(--gap) var(--gap);
background-color: #5d3fbd;
}
.mode-selector {
display: flex;
gap: 15px;
margin-bottom: 20px;
}
.mode-button {
border: none;
color: white;
text-align: center;
border-radius: 10px;
display: flex;
flex-direction: column;
align-items: center;
padding: 10px;
background-color: rgba(255, 255, 255, 0.1);
cursor: pointer;
transition: all 0.3s;
}
.mode-button:hover {
background-color: rgba(255, 255, 255, 0.2);
}
.mode-button.active {
background-color: rgba(255, 255, 255, 0.3);
}
.avatar-upload {
display: none;
flex-direction: column;
align-items: center;
gap: 10px;
margin-bottom: 20px;
}
.avatar-preview {
width: 80px;
height: 80px;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.1);
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.avatar-preview img {
width: 100%;
height: 100%;
object-fit: cover;
}
.upload-btn {
background-color: rgba(255, 255, 255, 0.1);
border: none;
color: white;
padding: 8px 15px;
border-radius: 5px;
cursor: pointer;
transition: all 0.3s;
}
.upload-btn:hover {
background-color: rgba(255, 255, 255, 0.2);
}
</style>
</head>
<body>
<div>
<h1 style="font-weight: 400; margin-bottom:20px;"> Recorder </h1>
<!-- Camera/Avatar Mode Selector -->
<div class="mode-selector">
<button id="camera-mode" class="mode-button active">
<i class="fas fa-camera fa-lg"></i>
<p>Camera</p>
</button>
<button id="avatar-mode" class="mode-button">
<i class="fas fa-user-circle fa-lg"></i>
<p>Avatar</p>
</button>
</div>
<!-- Avatar Upload Section -->
<div id="avatar-section" class="avatar-upload">
<div class="avatar-preview">
<img id="avatar-img" src="default-avatar.png" alt="Avatar">
</div>
<input type="file" id="avatar-input" accept="image/*" style="display: none;">
<button class="upload-btn" onclick="document.getElementById('avatar-input').click()">
Upload Avatar
</button>
</div>
<!-- Recording Options -->
<div style="display: flex; flex-direction: row; gap: 25px;">
<div style="display: flex; color: white;">
<button id="tab"
style="border: none; gap:10px; color: white; text-align: center; border-radius: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; padding:8px 10px; background-color: transparent; cursor: pointer;">
<i id="tab-icon" style="color: white;" class="fa-regular fa-window-maximize fa-2xl"></i>
<p>Window Tab</p>
</button>
</div>
<div style="display: flex; color: white;">
<button id="screen"
style="border: none; color: white; gap:10px; text-align: center; border-radius: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center; padding:8px 10px; background-color: transparent; cursor: pointer;">
<i id="screen-icon" style="color: white;" class="fa-solid fa-display fa-2xl"></i>
<p>Screen</p>
</button>
</div>
</div>
<div id="options">
<br>
<select id="quality"
style="background-color: transparent; color: white; border: 1.5px solid rgb(255, 255, 255);font-family: Poppins; padding: 3px; border-radius: 5px;"
required>
<option style="color: black; font-size:0.75rem; padding:8px 2px; cursor:pointer;" value="high" selected>
High (1080p, 60fps)</option>
</select>
</div>
</div>
<script src="popup.js"></script>
</body>
</html>

162
popup.js Normal file
View File

@ -0,0 +1,162 @@
//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');
avatarSection.style.display = 'none';
chrome.storage.local.set({ mode: 'camera' });
chrome.runtime.sendMessage({ type: 'mode-change', mode: 'camera' });
});
avatarMode.addEventListener('click', () => {
avatarMode.classList.add('active');
cameraMode.classList.remove('active');
avatarSection.style.display = 'flex';
chrome.storage.local.set({ mode: 'avatar' });
chrome.runtime.sendMessage({ type: 'mode-change', mode: 'avatar' });
});
// Avatar upload handling
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 });
chrome.runtime.sendMessage({
type: 'avatar-update',
avatarData: avatarData
});
};
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;
}
}
});
});
const recordTab = document.querySelector("#tab");
const recordScreen = document.querySelector("#screen");
const qualitySelect = document.querySelector('#quality');
const injectCamera = async () => {
const tab = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tab) return;
const tabId = tab[0].id;
console.log("Injecting into tab", tabId);
await chrome.scripting.executeScript({
files: ["content.js"],
target: { tabId },
});
};
// Initialize camera bubble when popup opens
injectCamera();
const removeCamera = async () => {
const tab = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tab) return;
const tabId = tab[0].id;
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";
},
target: { tabId },
});
};
const checkRecording = async () => {
const recording = await chrome.storage.local.get(["recording", "type"]);
const recordingStatus = recording.recording || false;
const recordingType = recording.type || "";
console.log("recording status", recordingStatus, recordingType);
return [recordingStatus, recordingType];
};
const init = async () => {
const recordingState = await checkRecording();
console.log("recording state", recordingState);
if (recordingState[0] === true) {
document.querySelector("#options").style.display = "none";
if (recordingState[1] === "tab") {
document.getElementById("tab-icon").classList.remove("fa-window-maximize");
document.getElementById("tab-icon").classList.remove("fa-regular");
document.getElementById("tab-icon").classList.add("fa-solid");
document.getElementById("tab-icon").classList.add("fa-stop");
} else {
document.getElementById("screen-icon").classList.remove("fa-display");
document.getElementById('screen-icon').classList.add("fa-stop");
}
} else {
document.querySelector("#options").style.display = "block";
}
const updateRecording = async (type) => {
console.log("start recording", type);
const quality = qualitySelect.value;
const recordingState = await checkRecording();
if (recordingState[0] === true) {
chrome.runtime.sendMessage({ type: "stop-recording" });
removeCamera();
} else {
chrome.runtime.sendMessage({
type: "start-recording",
recordingType: type,
quality: quality
});
injectCamera();
}
setTimeout(() => {
window.close();
}, 100);
};
recordTab.addEventListener("click", async () => {
console.log("updateRecording tab clicked");
updateRecording("tab");
});
recordScreen.addEventListener("click", async () => {
console.log("updateRecording screen clicked");
updateRecording("screen");
});
};
init();

251
service-worker.js Normal file
View File

@ -0,0 +1,251 @@
//service worker.js
const checkRecording = async () => {
const recording = await chrome.storage.local.get(["recording", "type"]);
const recordingStatus = recording.recording || false;
const recordingType = recording.type || "";
console.log("recording status", recordingStatus, recordingType);
return [recordingStatus, recordingType];
};
const updateRecording = async (state, type) => {
console.log("update recording", type);
chrome.storage.local.set({ recording: state, type });
};
const injectCamera = async () => {
const tabs = await chrome.tabs.query({});
for (const tab of tabs) {
if (tab.url.startsWith("chrome://") || tab.url.startsWith("chrome-extension://")) {
continue;
}
console.log("Injecting camera into tab", tab.id);
await chrome.scripting.executeScript({
files: ["content.js"],
target: { tabId: tab.id },
});
}
};
const removeCamera = async () => {
const tab = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tab) return;
const tabId = tab[0].id;
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";
},
target: { tabId },
});
};
chrome.tabs.onActivated.addListener(async (activeInfo) => {
console.log("tab activated", activeInfo);
const activeTab = await chrome.tabs.get(activeInfo.tabId);
if (!activeTab) return;
const tabUrl = activeTab.url;
if (
tabUrl.startsWith("chrome://") ||
tabUrl.startsWith("chrome-extension://")
) {
console.log("chrome or extension page - exiting");
return;
}
const [recording, recordingType] = await checkRecording();
console.log("recording check after tab change", {
recording,
recordingType,
tabUrl,
});
if (recording && recordingType === "screen") {
// inject the camera
injectCamera();
} else {
// remove the camera
removeCamera();
}
});
const startRecording = async (type, quality) => {
console.log("start recording", type);
const currentstate = await checkRecording();
console.log("current state", currentstate);
updateRecording(true, type);
const afterState = await checkRecording();
console.log("cuurent 2 state", afterState);
chrome.action.setIcon({ path: "icons/recording.png" });
if (type === "tab") {
recordTabState(true, quality);
}
if (type === "screen") {
recordScreen(quality);
}
};
const recordScreen = async (quality) => {
const desktopRecordPath = chrome.runtime.getURL("desktopRecord.html");
const currentTab = await chrome.tabs.query({
active: true,
currentWindow: true,
});
const currentTabId = currentTab[0].id;
const newTab = await chrome.tabs.create({
url: desktopRecordPath,
pinned: true,
active: true,
index: 0,
});
setTimeout(() => {
chrome.tabs.sendMessage(newTab.id, {
type: "start-recording",
focusedTabId: currentTabId,
quality: quality
});
}, 500);
};
const removeCameraFromAllTabs = async () => {
const tabs = await chrome.tabs.query({});
for (const tab of tabs) {
if (tab.url.startsWith("chrome://") || tab.url.startsWith("chrome-extension://")) {
continue;
}
console.log("Removing camera from tab", tab.id);
await chrome.scripting.executeScript({
func: () => {
const camera = document.querySelector("#purple-camera");
if (camera) {
camera.remove();
}
},
target: { tabId: tab.id },
});
}
};
const stopRecording = async () => {
console.log("stop recording");
await updateRecording(false, "");
await removeCameraFromAllTabs();
chrome.action.setIcon({ path: "icons/not-recording.png" });
await recordTabState(false);
};
const recordTabState = async (start = true, quality) => {
const existingContexts = await chrome.runtime.getContexts({});
const offscreenDocument = existingContexts.find(
(c) => c.contextType === "OFFSCREEN_DOCUMENT"
);
if (!offscreenDocument) {
// Create an offscreen document.
await chrome.offscreen.createDocument({
url: "offscreen.html",
reasons: ["USER_MEDIA", "DISPLAY_MEDIA"],
justification: "Recording from chrome.tabCapture API",
});
}
if (start) {
const tab = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tab) return;
const tabId = tab[0].id;
console.log("tab id", tabId);
const streamId = await chrome.tabCapture.getMediaStreamId({
targetTabId: tabId,
});
console.log("stream id", streamId);
// send to offscreen document
chrome.runtime.sendMessage({
type: "start-recording",
target: "offscreen",
data: streamId,
quality: quality
});
} else {
chrome.runtime.sendMessage({
type: "stop-recording",
target: "offscreen",
});
}
};
const openTabWithVideo = async (message) => {
console.log("request to open tab with video", message);
const { url: videoUrl, base64 } = message;
if (!videoUrl && !base64) return;
const url = chrome.runtime.getURL("video.html");
const newTab = await chrome.tabs.create({ url });
// send message to tab
setTimeout(() => {
chrome.tabs.sendMessage(newTab.id, {
type: "play-video",
videoUrl,
base64,
});
}, 500);
};
chrome.runtime.onMessage.addListener(async (request, sender, sendResponse) => {
console.log("message received", request, sender);
switch (request.type) {
case "open-tab":
await openTabWithVideo(request);
sendResponse({ status: "done" });
break;
case "start-recording":
await startRecording(request.recordingType, request.quality);
sendResponse({ status: "recording-started" });
break;
case "stop-recording":
await stopRecording();
sendResponse({ status: "recording-stopped" });
break;
default:
console.log("default");
sendResponse({ status: "unknown-message" });
}
return true;
});

432
video-upload.js Normal file
View File

@ -0,0 +1,432 @@
class YouTubeUploader {
constructor() {
this.CLIENT_ID = '728787049181-iq4lnrcks0fifee7r6h57h7h71berii6.apps.googleusercontent.com';
this.SCOPES = ['https://www.googleapis.com/auth/youtube.upload'];
this.initializeUploadButton();
}
initializeUploadButton() {
const uploadButton = document.getElementById('upload-youtube');
console.log('Upload button found:', !!uploadButton);
if (uploadButton) {
uploadButton.addEventListener('click', async (e) => {
console.log('Upload button clicked');
e.preventDefault();
try {
await this.prepareAndUploadVideo();
} catch (error) {
console.error('Complete upload error:', error);
this.showError(`Upload failed: ${error.message}`);
}
});
} else {
console.error('YouTube upload button not found in DOM');
}
}
async prepareAndUploadVideo() {
try {
// Detailed logging for video retrieval
const videoData = await this.getRecordedVideoFromStorage();
console.log('Video data retrieved:', !!videoData);
if (!videoData) {
this.showError('No video available to upload');
return;
}
// Convert base64 to blob with detailed logging
const videoBlob = this.base64ToBlob(videoData);
console.log('Video blob created, size:', videoBlob.size);
// Get authentication token
const token = await this.getAuthToken();
console.log('Authentication token obtained');
// Prepare metadata for the video
const metadata = {
snippet: {
title: `Screen Recording ${new Date().toLocaleString()}`,
description: 'Screen recording uploaded from Chrome Extension',
tags: ['screen recording'],
categoryId: '22' // Category for 'People & Blogs'
},
status: {
privacyStatus: 'private'
}
};
// Perform the upload
const uploadResult = await this.uploadVideo(token, videoBlob, metadata);
console.log('Upload result:', uploadResult);
this.showSuccess('Video uploaded to YouTube successfully!');
} catch (error) {
console.error('Complete YouTube Upload Error:', error);
this.showError(`Upload failed: ${error.message}`);
}
}
getRecordedVideoFromStorage() {
return new Promise((resolve, reject) => {
chrome.storage.local.get(['recordedVideoData'], (result) => {
console.log('Storage retrieval:', result.recordedVideoData ? 'Video found' : 'No video');
if (chrome.runtime.lastError) {
reject(chrome.runtime.lastError);
} else {
resolve(result.recordedVideoData);
}
});
});
}
getAuthToken() {
return new Promise((resolve, reject) => {
console.log('Attempting to get auth token');
// Fallback authentication method
const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?${new URLSearchParams({
client_id: this.CLIENT_ID,
redirect_uri: chrome.identity.getRedirectURL(),
response_type: 'token',
scope: this.SCOPES.join(' '),
prompt: 'consent'
})}`;
chrome.identity.launchWebAuthFlow(
{ url: authUrl, interactive: true },
(redirectUrl) => {
console.log('Auth flow redirect received');
if (chrome.runtime.lastError) {
console.error('Auth flow error:', chrome.runtime.lastError);
reject(chrome.runtime.lastError);
return;
}
const urlParams = new URLSearchParams(new URL(redirectUrl).hash.slice(1));
const accessToken = urlParams.get('access_token');
if (!accessToken) {
console.error('No access token retrieved');
reject(new Error('Failed to retrieve access token'));
return;
}
console.log('Access token successfully retrieved');
resolve(accessToken);
}
);
});
}
base64ToBlob(base64Data) {
// Remove the data URL prefix if it exists
const base64String = base64Data.replace(/^data:video\/\w+;base64,/, '');
// Decode base64
const byteCharacters = atob(base64String);
const byteArrays = [];
for (let offset = 0; offset < byteCharacters.length; offset += 512) {
const slice = byteCharacters.slice(offset, offset + 512);
const byteNumbers = new Array(slice.length);
for (let i = 0; i < slice.length; i++) {
byteNumbers[i] = slice.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
byteArrays.push(byteArray);
}
return new Blob(byteArrays, { type: 'video/webm' });
}
async uploadVideo(token, videoBlob, metadata) {
const formData = new FormData();
const metadataBlob = new Blob([JSON.stringify(metadata)], {
type: 'application/json; charset=UTF-8'
});
formData.append('metadata', metadataBlob, 'metadata.json');
formData.append('file', videoBlob, 'screen_recording.webm');
try {
const response = await fetch(
`https://www.googleapis.com/upload/youtube/v3/videos?uploadType=multipart&part=snippet,status`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`
},
body: formData
}
);
if (!response.ok) {
const errorBody = await response.text();
console.error('Upload response error:', errorBody);
throw new Error(`Upload failed: ${errorBody}`);
}
const result = await response.json();
console.log('YouTube Upload Success:', result);
return result;
} catch (error) {
console.error('Upload Error Details:', error);
throw error;
}
}
showSuccess(message) {
const notification = document.createElement('div');
notification.textContent = message;
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background-color: green;
color: white;
padding: 10px 20px;
border-radius: 5px;
z-index: 1000;
`;
document.body.appendChild(notification);
setTimeout(() => notification.remove(), 3000);
}
showError(message) {
const notification = document.createElement('div');
notification.textContent = message;
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background-color: red;
color: white;
padding: 10px 20px;
border-radius: 5px;
z-index: 1000;
`;
document.body.appendChild(notification);
setTimeout(() => notification.remove(), 3000);
}
}
// Initialize the uploader when the page loads
document.addEventListener('DOMContentLoaded', () => {
console.log('DOM loaded, initializing YouTube Uploader');
new YouTubeUploader();
});
// Add global error logging
window.addEventListener('error', (event) => {
console.error('Unhandled error:', event.error);
});
class VimeoUploader {
constructor() {
// Replace with your Vimeo API access token
this.ACCESS_TOKEN = 'fad52305c371058da84097cefb9a95a3';
this.initializeUploadButton();
}
initializeUploadButton() {
const uploadButton = document.getElementById('upload-vimeo');
console.log('Vimeo upload button found:', !!uploadButton);
if (uploadButton) {
uploadButton.addEventListener('click', async (e) => {
console.log('Vimeo upload button clicked');
e.preventDefault();
try {
await this.prepareAndUploadVideo();
} catch (error) {
console.error('Vimeo upload error:', error);
this.showError(`Upload failed: ${error.message}`);
}
});
} else {
console.error('Vimeo upload button not found');
}
}
async prepareAndUploadVideo() {
try {
// Retrieve video from storage
const videoData = await this.getRecordedVideoFromStorage();
console.log('Video data retrieved:', !!videoData);
if (!videoData) {
this.showError('No video available to upload');
return;
}
// Convert base64 to blob
const videoBlob = this.base64ToBlob(videoData);
console.log('Video blob created, size:', videoBlob.size);
// Initiate upload and get upload link
const uploadTicket = await this.createUploadTicket(videoBlob.size);
// Upload the video
await this.uploadVideoToVimeo(uploadTicket.upload_link, videoBlob);
this.showSuccess('Video uploaded to Vimeo successfully!');
} catch (error) {
console.error('Vimeo Upload Error:', error);
this.showError(`Upload failed: ${error.message}`);
}
}
async createUploadTicket(fileSize) {
try {
const response = await fetch('https://api.vimeo.com/me/videos', {
method: 'POST',
headers: {
'Authorization': `bearer ${this.ACCESS_TOKEN}`,
'Content-Type': 'application/json',
'Accept': 'application/vnd.vimeo.*+json;version=3.4'
},
body: JSON.stringify({
upload: {
approach: 'tus',
size: fileSize
},
name: `Screen Recording ${new Date().toLocaleString()}`,
description: 'Screen recording uploaded from Chrome Extension'
})
});
if (!response.ok) {
const errorText = await response.text();
console.error('Vimeo API Response:', errorText);
throw new Error(`Failed to create upload ticket: ${errorText}`);
}
const uploadTicket = await response.json();
console.log('Complete Upload Ticket:', uploadTicket);
// Explicitly log the upload link
const uploadLink = uploadTicket.upload?.upload_link;
console.log('Extracted Upload Link:', uploadLink);
if (!uploadLink) {
throw new Error('No upload link found in Vimeo response');
}
return uploadTicket;
} catch (error) {
console.error('Upload Ticket Creation Error:', error);
throw error;
}
}
async uploadVideoToVimeo(uploadTicket, videoBlob) {
const uploadLink = uploadTicket.upload
? uploadTicket.upload.upload_link
: uploadTicket.upload_link || uploadTicket.uri;
console.log('Actual Upload Link:', uploadLink);
if (!uploadLink) {
throw new Error('No upload link found');
}
try {
const response = await fetch(uploadLink, {
method: 'PATCH',
headers: {
'Content-Type': 'application/offset+octet-stream',
'Upload-Offset': '0',
'Tus-Resumable': '1.0.0',
'Authorization': `bearer ${this.ACCESS_TOKEN}`
},
body: videoBlob
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Vimeo upload failed: ${errorText}`);
}
return response;
} catch (error) {
console.error('Upload detailed error:', error);
throw error;
}
}
getRecordedVideoFromStorage() {
return new Promise((resolve, reject) => {
chrome.storage.local.get(['recordedVideoData'], (result) => {
console.log('Storage retrieval:', result.recordedVideoData ? 'Video found' : 'No video');
if (chrome.runtime.lastError) {
reject(chrome.runtime.lastError);
} else {
resolve(result.recordedVideoData);
}
});
});
}
base64ToBlob(base64Data) {
const base64String = base64Data.replace(/^data:video\/\w+;base64,/, '');
const byteCharacters = atob(base64String);
const byteArrays = [];
for (let offset = 0; offset < byteCharacters.length; offset += 512) {
const slice = byteCharacters.slice(offset, offset + 512);
const byteNumbers = new Array(slice.length);
for (let i = 0; i < slice.length; i++) {
byteNumbers[i] = slice.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
byteArrays.push(byteArray);
}
return new Blob(byteArrays, { type: 'video/webm' });
}
showSuccess(message) {
const notification = document.createElement('div');
notification.textContent = message;
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background-color: green;
color: white;
padding: 10px 20px;
border-radius: 5px;
z-index: 1000;
`;
document.body.appendChild(notification);
setTimeout(() => notification.remove(), 3000);
}
showError(message) {
const notification = document.createElement('div');
notification.textContent = message;
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background-color: red;
color: white;
padding: 10px 20px;
border-radius: 5px;
z-index: 1000;
`;
document.body.appendChild(notification);
setTimeout(() => notification.remove(), 3000);
}
}
// Initialize the uploader when the page loads
document.addEventListener('DOMContentLoaded', () => {
console.log('DOM loaded, initializing Vimeo Uploader');
new VimeoUploader();
});

226
video.html Normal file
View File

@ -0,0 +1,226 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
<title>Video Editor</title>
<style>
body {
margin: 0;
padding: 20px;
background-color: #43138bb9;
color: white;
font-family: Arial, sans-serif;
min-height: 100vh;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
#video-container {
display: flex;
flex-direction: column;
align-items: center;
background: rgba(93, 63, 189, 0.1);
padding: 20px;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
#recorded-video {
max-width: 80vw;
max-height: 70vh;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
}
#timeline {
width: 80vw;
height: 50px;
background: #5d3fbd;
margin: 20px 0;
position: relative;
cursor: pointer;
border-radius: 25px;
overflow: hidden;
}
#trim-start, #trim-end {
position: absolute;
height: 100%;
width: 10px;
background: rgba(159, 140, 189, 0.767);
cursor: pointer;
transition: background 0.3s ease;
}
#controls {
margin-top: 20px;
display: flex;
gap: 15px;
flex-wrap: wrap;
justify-content: center;
}
.btn,
button {
background-color: #5d3fbd;
color: white;
border: none;
padding: 12px 24px;
border-radius: 25px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 8px;
}
.btn:hover,
button:hover {
background-color: #4d2fa7;
transform: translateY(-2px);
}
.btn:disabled,
button:disabled {
background-color: #999;
cursor: not-allowed;
transform: none;
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.8);
z-index: 1000;
justify-content: center;
align-items: center;
}
.modal-content {
background: white;
padding: 30px;
border-radius: 10px;
width: 90%;
max-width: 500px;
color: #333;
position: relative;
}
/* Updated button container styles */
.modal-buttons {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 20px;
}
/* Button styles */
.modal-buttons .btn {
background-color: #5d3fbd;
color: white;
padding: 12px 24px;
border: none;
border-radius: 25px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.3s ease;
}
.modal-buttons .btn:hover {
background-color: #4d2fa7;
transform: translateY(-2px);
}
/* Textarea and input styles */
.modal textarea,
.modal input {
width: 100%;
padding: 10px;
margin: 10px 0;
border: 1px solid #ddd;
border-radius: 5px;
font-family: monospace;
}
.progress-bar {
width: 100%;
height: 10px;
background: #eee;
border-radius: 5px;
margin: 10px 0;
overflow: hidden;
}
.progress-bar-fill {
height: 100%;
background: #5d3fbd;
width: 0%;
transition: width 0.3s ease;
}
</style>
</head>
<body>
<div class="container">
<div id="video-container">
<video id="recorded-video" controls></video>
<div id="timeline">
<div id="trim-start"></div>
<div id="trim-end"></div>
</div>
<div id="controls">
<button class="btn" id="download-btn">
<i class="fas fa-download"></i>
Download
</button>
<button class="btn" id="trim-btn">
<i class="fas fa-cut"></i>
Trim Video
</button>
<button class="btn upload-btn-youtube" id="upload-youtube">
<i class="fab fa-youtube"></i>
Upload to YouTube
</button>
<button class="btn upload-btn-vimeo" id="upload-vimeo">
<i class="fab fa-vimeo-v"></i>
Upload to Vimeo
</button>
</div>
</div>
<!-- Embed Code Modal -->
<div id="embed-modal" class="modal">
<div class="modal-content" id="embed-btn">
<i class="fas fa-code"></i>
<h2>Embed Code</h2>
<p>Copy and paste this code to embed the video on your website:</p>
<textarea id="embed-code" rows="4" readonly></textarea>
<div class="modal-buttons">
<button class="btn" id="copy-embed">Copy Code</button>
<button class="btn" id="close-embed-modal">Close</button>
</div>
</div>
</div>
<!-- Font Awesome for icons -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/js/all.min.js"></script>
<script src="https://apis.google.com/js/api.js"></script>
<script src="video.js"></script>
<script src="video-upload.js"></script>
</body>
</html>

453
video.js Normal file
View File

@ -0,0 +1,453 @@
//video.js
let videoBlob = null;
let videoDuration = 0;
let mediaRecorder = null;
let recordedChunks = [];
let currentVideoUrl = null;
const timeline = document.getElementById('timeline');
const trimStart = document.getElementById('trim-start');
const trimEnd = document.getElementById('trim-end');
const videoElement = document.querySelector("#recorded-video");
const downloadBtn = document.querySelector("#download-btn");
const trimBtn = document.querySelector("#trim-btn");
let isDraggingStart = false;
let isDraggingEnd = false;
trimStart.style.left = '0%';
trimEnd.style.left = '100%';
trimBtn.disabled = true;
trimStart.addEventListener('mousedown', startDragStart);
trimEnd.addEventListener('mousedown', startDragEnd);
document.addEventListener('mousemove', drag);
document.addEventListener('mouseup', stopDrag);
function startDragStart(e) {
isDraggingStart = true;
e.preventDefault();
}
function startDragEnd(e) {
isDraggingEnd = true;
e.preventDefault();
}
function drag(e) {
if (!isDraggingStart && !isDraggingEnd) return;
const timelineRect = timeline.getBoundingClientRect();
let newPosition = ((e.clientX - timelineRect.left) / timelineRect.width) * 100;
newPosition = Math.max(0, Math.min(newPosition, 100));
if (isDraggingStart) {
const endPosition = parseFloat(trimEnd.style.left) || 100;
if (newPosition >= endPosition) return;
trimStart.style.left = `${newPosition}%`;
updateVideoTime(newPosition, 'start');
}
if (isDraggingEnd) {
const startPosition = parseFloat(trimStart.style.left) || 0;
if (newPosition <= startPosition) return;
trimEnd.style.left = `${newPosition}%`;
updateVideoTime(newPosition, 'end');
}
}
function updateVideoTime(position, type) {
if (!videoDuration || !isFinite(videoDuration)) return;
const timeInSeconds = (position / 100) * videoDuration;
if (isFinite(timeInSeconds) && type === 'start') {
videoElement.currentTime = timeInSeconds;
}
}
function stopDrag() {
isDraggingStart = false;
isDraggingEnd = false;
}
async function waitForVideoDuration() {
return new Promise((resolve) => {
const checkDuration = () => {
if (videoElement.readyState >= 2 && isFinite(videoElement.duration)) {
videoDuration = videoElement.duration;
resolve(videoDuration);
} else {
setTimeout(checkDuration, 100);
}
};
checkDuration();
});
}
async function trimVideo(startTime, endTime) {
// Validate inputs
if (!isFinite(startTime) || !isFinite(endTime) || startTime < 0 || endTime <= startTime) {
throw new Error('Invalid trim times');
}
return new Promise((resolve, reject) => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// Set canvas dimensions to match video
canvas.width = videoElement.videoWidth;
canvas.height = videoElement.videoHeight;
// Create a new MediaRecorder
const stream = canvas.captureStream();
try {
mediaRecorder = new MediaRecorder(stream, {
mimeType: 'video/webm;codecs=vp8',
videoBitsPerSecond: 2500000
});
} catch (e) {
mediaRecorder = new MediaRecorder(stream, {
mimeType: 'video/webm',
videoBitsPerSecond: 2500000
});
}
recordedChunks = [];
mediaRecorder.ondataavailable = (e) => {
if (e.data.size > 0) {
recordedChunks.push(e.data);
}
};
mediaRecorder.onstop = () => {
const blob = new Blob(recordedChunks, { type: 'video/webm' });
resolve(blob);
};
// Start recording
mediaRecorder.start(100);
// Set video to start time
videoElement.currentTime = startTime;
const drawFrame = () => {
if (videoElement.currentTime >= endTime) {
mediaRecorder.stop();
return;
}
ctx.drawImage(videoElement, 0, 0, canvas.width, canvas.height);
videoElement.currentTime += 1/30;
requestAnimationFrame(drawFrame);
};
videoElement.onseeked = () => {
if (Math.abs(videoElement.currentTime - startTime) < 0.1) {
drawFrame();
videoElement.onseeked = null;
}
};
});
}
async function playVideo(message) {
const url = message?.videoUrl || message?.base64;
if (!url) {
console.error('No video URL provided');
return;
}
try {
// Clean up previous video if it exists
if (currentVideoUrl) {
URL.revokeObjectURL(currentVideoUrl);
currentVideoUrl = null;
}
// Reset video element
videoElement.pause();
videoElement.currentTime = 0;
videoElement.src = '';
// Clear previous video data
videoBlob = null;
videoDuration = 0;
// Load new video blob
const response = await fetch(url);
videoBlob = await response.blob();
// Create and store new object URL
currentVideoUrl = URL.createObjectURL(videoBlob);
videoElement.src = currentVideoUrl;
// Wait for video metadata and duration
await waitForVideoDuration();
console.log('Video duration:', videoDuration);
// Reset trim handles
trimStart.style.left = '0%';
trimEnd.style.left = '100%';
// Enable trim button
trimBtn.disabled = false;
// Clear any stored video URL
if (chrome?.storage?.local) {
chrome.storage.local.remove("videoUrl");
}
// Store new video URL
if (url !== currentVideoUrl) {
saveVideo(url);
}
} catch (error) {
console.error('Error loading video:', error);
// Clean up on error
if (currentVideoUrl) {
URL.revokeObjectURL(currentVideoUrl);
currentVideoUrl = null;
}
return;
}
// Update download functionality
// Update download functionality
downloadBtn.onclick = () => {
if (videoBlob) {
try {
// Create a download link
const a = document.createElement('a');
a.href = currentVideoUrl;
// Set a default filename with timestamp
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
a.download = `recorded_video_${timestamp}.webm`;
// Make sure the link is hidden
a.style.display = 'none';
// Add to document, click it, and remove it
document.body.appendChild(a);
a.click();
// Small timeout before removing the element
setTimeout(() => {
document.body.removeChild(a);
}, 100);
} catch (error) {
console.error('Download error:', error);
alert('Error downloading video. Please try again.');
}
} else {
alert('No video available to download');
}
};
// Update trim functionality
trimBtn.onclick = async () => {
if (!videoBlob || !videoDuration) {
console.error('Video not properly loaded');
return;
}
try {
const startPercent = parseFloat(trimStart.style.left) || 0;
const endPercent = parseFloat(trimEnd.style.left) || 100;
const startTime = (startPercent / 100) * videoDuration;
const endTime = (endPercent / 100) * videoDuration;
if (!isFinite(startTime) || !isFinite(endTime)) {
throw new Error('Invalid trim times calculated');
}
console.log(`Trimming video from ${startTime}s to ${endTime}s`);
trimBtn.disabled = true;
trimBtn.textContent = 'Trimming...';
const trimmedBlob = await trimVideo(startTime, endTime);
// Clean up previous video
if (currentVideoUrl) {
URL.revokeObjectURL(currentVideoUrl);
}
// Set up new video
videoBlob = trimmedBlob;
currentVideoUrl = URL.createObjectURL(trimmedBlob);
videoElement.src = currentVideoUrl;
console.log('Video trimmed successfully');
} catch (error) {
console.error('Error trimming video:', error.message);
} finally {
trimBtn.disabled = false;
trimBtn.textContent = 'Trim Video';
}
};
}
// Storage and message handling
const saveVideo = (videoUrl) => {
if (chrome?.storage?.local) {
chrome.storage.local.set({ videoUrl }, () => {
if (chrome.runtime.lastError) {
console.error('Error saving video URL:', chrome.runtime.lastError);
}
});
}
};
// Listen for stored video on load
if (chrome?.storage?.local) {
chrome.storage.local.get(["videoUrl"], (result) => {
if (result.videoUrl) {
playVideo(result);
}
});
}
// Listen for messages from service worker
if (chrome?.runtime?.onMessage) {
chrome.runtime.onMessage.addListener((message) => {
switch (message.type) {
case "play-video":
playVideo(message);
break;
default:
console.log("Unknown message type");
}
});
}
window.addEventListener('unload', () => {
if (currentVideoUrl) {
URL.revokeObjectURL(currentVideoUrl);
}
});
// Add these variables at the top of video.js
// Add these variables at the top of video.js
let embedCode = '';
// Make sure this is defined
// Function to generate embed code
function generateEmbedCode(videoUrl) {
const embedWidth = 640;
const embedHeight = 360;
return `<iframe width="${embedWidth}" height="${embedHeight}" src="${videoUrl}" frameborder="0" allowfullscreen></iframe>`;
}
// Function to show embed modal
function showEmbedModal() {
const embedModal = document.getElementById('embed-modal');
const embedCodeTextarea = document.getElementById('embed-code');
const copyEmbedBtn = document.getElementById('copy-embed');
const closeEmbedModalBtn = document.getElementById('close-embed-modal');
if (videoBlob) {
const videoUrl = URL.createObjectURL(videoBlob);
embedCode = generateEmbedCode(videoUrl);
embedCodeTextarea.value = embedCode;
embedModal.style.display = 'flex';
// Copy button functionality
copyEmbedBtn.onclick = () => {
embedCodeTextarea.select();
document.execCommand('copy');
copyEmbedBtn.innerHTML = '<i class="fas fa-check"></i> Copied!';
setTimeout(() => {
copyEmbedBtn.innerHTML = '<i class="fas fa-copy"></i> Copy Code';
}, 2000);
};
// Close button functionality
closeEmbedModalBtn.onclick = () => {
embedModal.style.display = 'none';
};
// Close modal when clicking outside
window.onclick = (event) => {
if (event.target === embedModal) {
embedModal.style.display = 'none';
}
};
}
}
// Add embed button to controls
function addEmbedButton() {
const controlsContainer = document.getElementById('controls');
const embedBtn = document.createElement('button');
embedBtn.className = 'btn';
embedBtn.innerHTML = '<i class="fas fa-code"></i> Get Embed Code';
embedBtn.onclick = showEmbedModal;
controlsContainer.appendChild(embedBtn);
}
// Initialize embed functionality
document.addEventListener('DOMContentLoaded', () => {
addEmbedButton();
});
// Additional CSS to add to your existing styles
const additionalStyles = `
.modal {
animation: fadeIn 0.3s ease-in-out;
}
.modal-content {
animation: slideIn 0.3s ease-in-out;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
}
#embed-code {
background: #f8f9fa;
font-family: 'Courier New', monospace;
font-size: 14px;
resize: none;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 15px;
margin: 15px 0;
transition: border-color 0.3s ease;
}
#embed-code:focus {
outline: none;
border-color: #5d3fbd;
}
.modal-buttons .btn {
min-width: 120px;
justify-content: center;
}
.modal-buttons .btn i {
margin-right: 8px;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideIn {
from {
transform: translateY(-20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
#copy-embed:not(:hover) i {
transition: transform 0.3s ease;
}
#copy-embed:hover i {
transform: translateY(-2px);
}
`;