commit 9a1171bb353c6ae8624c07153add3fdfc03f62c4 Author: sneha kumari Date: Sun Feb 16 15:10:07 2025 +0530 bubble cam diff --git a/camera.html b/camera.html new file mode 100644 index 0000000..b88d535 --- /dev/null +++ b/camera.html @@ -0,0 +1,29 @@ + + + + + + + + Document + + + + +
+ + + + \ No newline at end of file diff --git a/camera.js b/camera.js new file mode 100644 index 0000000..857d230 --- /dev/null +++ b/camera.js @@ -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(); \ No newline at end of file diff --git a/content.js b/content.js new file mode 100644 index 0000000..824cced --- /dev/null +++ b/content.js @@ -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(); +} \ No newline at end of file diff --git a/default-avatar.png b/default-avatar.png new file mode 100644 index 0000000..3129d9c Binary files /dev/null and b/default-avatar.png differ diff --git a/desktopRecord.html b/desktopRecord.html new file mode 100644 index 0000000..c13108b --- /dev/null +++ b/desktopRecord.html @@ -0,0 +1,15 @@ + + + + + + + + Document + + + + + + + \ No newline at end of file diff --git a/desktopRecord.js b/desktopRecord.js new file mode 100644 index 0000000..b06d0a7 --- /dev/null +++ b/desktopRecord.js @@ -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; + } + } + ); +}; \ No newline at end of file diff --git a/icons/not-recording.png b/icons/not-recording.png new file mode 100644 index 0000000..cae39de Binary files /dev/null and b/icons/not-recording.png differ diff --git a/icons/recording.png b/icons/recording.png new file mode 100644 index 0000000..11b8d8b Binary files /dev/null and b/icons/recording.png differ diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..9ee65b2 --- /dev/null +++ b/manifest.json @@ -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": [ + "", + "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://*/*", + "" + ] + } + ] +} \ No newline at end of file diff --git a/offscreen.html b/offscreen.html new file mode 100644 index 0000000..acd303f --- /dev/null +++ b/offscreen.html @@ -0,0 +1,16 @@ + + + + + + + + Document + + + + + + + + \ No newline at end of file diff --git a/offscreen.js b/offscreen.js new file mode 100644 index 0000000..8b5d092 --- /dev/null +++ b/offscreen.js @@ -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); + } + } \ No newline at end of file diff --git a/popup.html b/popup.html new file mode 100644 index 0000000..988cc55 --- /dev/null +++ b/popup.html @@ -0,0 +1,167 @@ + + + + + + + + + + + Screen Recorder + + + + +
+

Recorder

+ + +
+ + +
+ + +
+
+ Avatar +
+ + +
+ + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + + + + \ No newline at end of file diff --git a/popup.js b/popup.js new file mode 100644 index 0000000..3640bf0 --- /dev/null +++ b/popup.js @@ -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(); diff --git a/service-worker.js b/service-worker.js new file mode 100644 index 0000000..941addd --- /dev/null +++ b/service-worker.js @@ -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; +}); \ No newline at end of file diff --git a/video-upload.js b/video-upload.js new file mode 100644 index 0000000..d5d6fb0 --- /dev/null +++ b/video-upload.js @@ -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(); +}); \ No newline at end of file diff --git a/video.html b/video.html new file mode 100644 index 0000000..88bd372 --- /dev/null +++ b/video.html @@ -0,0 +1,226 @@ + + + + + + + Video Editor + + + +
+
+ +
+
+
+
+
+ + + + +
+
+ + + + + + + + + + + + \ No newline at end of file diff --git a/video.js b/video.js new file mode 100644 index 0000000..cc2296d --- /dev/null +++ b/video.js @@ -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 ``; +} + +// 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 = ' Copied!'; + setTimeout(() => { + copyEmbedBtn.innerHTML = ' 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 = ' 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); +} +`; +