bubble cam
This commit is contained in:
commit
9a1171bb35
29
camera.html
Normal file
29
camera.html
Normal 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
141
camera.js
Normal 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
88
content.js
Normal 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
BIN
default-avatar.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 289 KiB |
15
desktopRecord.html
Normal file
15
desktopRecord.html
Normal 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
227
desktopRecord.js
Normal 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
BIN
icons/not-recording.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 47 KiB |
BIN
icons/recording.png
Normal file
BIN
icons/recording.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 54 KiB |
63
manifest.json
Normal file
63
manifest.json
Normal 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
16
offscreen.html
Normal 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
182
offscreen.js
Normal 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
167
popup.html
Normal 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
162
popup.js
Normal 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
251
service-worker.js
Normal 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
432
video-upload.js
Normal 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
226
video.html
Normal 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
453
video.js
Normal 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);
|
||||
}
|
||||
`;
|
||||
|
Loading…
x
Reference in New Issue
Block a user