├── README.md ├── camera.html ├── camera.js ├── content.js ├── desktopRecord.html ├── desktopRecord.js ├── icons ├── not-recording.png └── recording.png ├── manifest.json ├── offscreen.html ├── offscreen.js ├── popup.html ├── popup.js ├── prettierrc.yaml ├── service-worker.js ├── video.html └── video.js /README.md: -------------------------------------------------------------------------------- 1 | # screen-rec 2 | 3 | full readme coming soon - TLDR feel free to submit a PR. 4 | -------------------------------------------------------------------------------- /camera.html: -------------------------------------------------------------------------------- 1 | 13 | 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /camera.js: -------------------------------------------------------------------------------- 1 | const runCode = async () => { 2 | const camaeraElement = document.querySelector("#camera"); 3 | 4 | // first request permission to use camera and microphone 5 | const permissions = await navigator.permissions.query({ 6 | name: "camera", 7 | }); 8 | 9 | // prompt user to enable camera and microphone 10 | if (permissions.state === "prompt") { 11 | //trigger the permissions dialog 12 | await navigator.mediaDevices.getUserMedia({ audio: true, video: true }); 13 | return; 14 | } 15 | 16 | if (permissions.state === "denied") { 17 | alert("Camera permissions denied"); 18 | return; 19 | } 20 | 21 | const startCamera = async () => { 22 | const videoElement = document.createElement("video"); 23 | videoElement.setAttribute( 24 | "style", 25 | ` 26 | 27 | height:200px; 28 | border-radius: 100px; 29 | transform: scaleX(-1); 30 | ` 31 | ); 32 | videoElement.setAttribute("autoplay", true); 33 | videoElement.setAttribute("muted", true); 34 | 35 | const cameraStream = await navigator.mediaDevices.getUserMedia({ 36 | audio: false, 37 | video: true, 38 | }); 39 | 40 | videoElement.srcObject = cameraStream; 41 | 42 | camaeraElement.appendChild(videoElement); 43 | }; 44 | 45 | startCamera(); 46 | }; 47 | 48 | runCode(); 49 | -------------------------------------------------------------------------------- /content.js: -------------------------------------------------------------------------------- 1 | window.cameraId = "rusty-camera"; 2 | window.camera = document.getElementById(cameraId); 3 | 4 | // check if camera exists 5 | if (window.camera) { 6 | console.log("camera found", camera); 7 | // make sure it is visible 8 | document.querySelector("#rusty-camera").style.display = "block"; 9 | } else { 10 | const camaeraElement = document.createElement("iframe"); 11 | camaeraElement.id = cameraId; 12 | camaeraElement.setAttribute( 13 | "style", 14 | ` 15 | all: initial; 16 | position: fixed; 17 | width:200px; 18 | height:200px; 19 | top:10px; 20 | right:10px; 21 | border-radius: 100px; 22 | background: black; 23 | z-index: 999999; 24 | border:none; 25 | ` 26 | ); 27 | 28 | // set permiissions on iframe - camera and microphone 29 | camaeraElement.setAttribute("allow", "camera; microphone"); 30 | 31 | camaeraElement.src = chrome.runtime.getURL("camera.html"); 32 | document.body.appendChild(camaeraElement); 33 | document.querySelector("#rusty-camera").style.display = "block"; 34 | } 35 | -------------------------------------------------------------------------------- /desktopRecord.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /desktopRecord.js: -------------------------------------------------------------------------------- 1 | const convertBlobToBase64 = (blob) => { 2 | return new Promise((resolve) => { 3 | const reader = new FileReader(); 4 | reader.readAsDataURL(blob); 5 | reader.onloadend = () => { 6 | const base64data = reader.result; 7 | resolve(base64data); 8 | }; 9 | }); 10 | }; 11 | 12 | const fetchBlob = async (url) => { 13 | const response = await fetch(url); 14 | const blob = await response.blob(); 15 | const base64 = await convertBlobToBase64(blob); 16 | return base64; 17 | }; 18 | 19 | // listen for messages from the service worker - start recording - stop recording 20 | chrome.runtime.onMessage.addListener(function (request, sender) { 21 | console.log("message received", request, sender); 22 | 23 | switch (request.type) { 24 | case "start-recording": 25 | startRecording(request.focusedTabId); 26 | break; 27 | case "stop-recording": 28 | stopRecording(); 29 | break; 30 | default: 31 | console.log("default"); 32 | } 33 | 34 | return true; 35 | }); 36 | 37 | let recorder; 38 | let data = []; 39 | 40 | const stopRecording = () => { 41 | console.log("stop recording"); 42 | if (recorder?.state === "recording") { 43 | recorder.stop(); 44 | // stop all streams 45 | recorder.stream.getTracks().forEach((t) => t.stop()); 46 | } 47 | }; 48 | 49 | const startRecording = async (focusedTabId) => { 50 | //... 51 | // use desktopCapture to get the screen stream 52 | chrome.desktopCapture.chooseDesktopMedia( 53 | ["screen", "window"], 54 | async function (streamId) { 55 | if (streamId === null) { 56 | return; 57 | } 58 | // have stream id 59 | console.log("stream id from desktop capture", streamId); 60 | 61 | const stream = await navigator.mediaDevices.getUserMedia({ 62 | audio: { 63 | mandatory: { 64 | chromeMediaSource: "desktop", 65 | chromeMediaSourceId: streamId, 66 | }, 67 | }, 68 | video: { 69 | mandatory: { 70 | chromeMediaSource: "desktop", 71 | chromeMediaSourceId: streamId, 72 | }, 73 | }, 74 | }); 75 | 76 | console.log("stream from desktop capture", stream); 77 | 78 | // get the microphone stream 79 | const microphone = await navigator.mediaDevices.getUserMedia({ 80 | audio: { echoCancellation: false }, 81 | }); 82 | 83 | // check that the microphone stream has audio tracks 84 | if (microphone.getAudioTracks().length !== 0) { 85 | const combinedStream = new MediaStream([ 86 | stream.getVideoTracks()[0], 87 | microphone.getAudioTracks()[0], 88 | ]); 89 | 90 | console.log("combined stream", combinedStream); 91 | 92 | recorder = new MediaRecorder(combinedStream, { 93 | mimeType: "video/webm", 94 | }); 95 | 96 | // listen for data 97 | recorder.ondataavailable = (event) => { 98 | console.log("data available", event); 99 | data.push(event.data); 100 | }; 101 | 102 | // listen for when recording stops 103 | recorder.onstop = async () => { 104 | console.log("recording stopped"); 105 | // send the data to the service worker 106 | const blobFile = new Blob(data, { type: "video/webm" }); 107 | const base64 = await fetchBlob(URL.createObjectURL(blobFile)); 108 | 109 | // send message to service worker to open tab 110 | console.log("send message to open tab", base64); 111 | chrome.runtime.sendMessage({ type: "open-tab", base64 }); 112 | 113 | data = []; 114 | }; 115 | 116 | // start recording 117 | recorder.start(); 118 | 119 | // set focus back to the previous tab 120 | if (focusedTabId) { 121 | chrome.tabs.update(focusedTabId, { active: true }); 122 | } 123 | } 124 | 125 | return; 126 | } 127 | ); 128 | }; 129 | -------------------------------------------------------------------------------- /icons/not-recording.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustyzone/screen-rec/aa30048acea6724a8eac779f73eb3cb39f1131d6/icons/not-recording.png -------------------------------------------------------------------------------- /icons/recording.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustyzone/screen-rec/aa30048acea6724a8eac779f73eb3cb39f1131d6/icons/recording.png -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Rusty Recorder", 3 | "description": "Records the current tab in an offscreen document & the whole screen", 4 | "version": "1", 5 | "manifest_version": 3, 6 | "minimum_chrome_version": "116", 7 | "action": { 8 | "default_icon": { 9 | "16": "icons/not-recording.png", 10 | "32": "icons/not-recording.png" 11 | }, 12 | "default_popup": "popup.html" 13 | }, 14 | "host_permissions": ["https://*/*", "http://*/*"], 15 | "background": { 16 | "service_worker": "service-worker.js" 17 | }, 18 | "permissions": [ 19 | "tabCapture", 20 | "offscreen", 21 | "scripting", 22 | "storage", 23 | "desktopCapture", 24 | "tabs" 25 | ], 26 | "web_accessible_resources": [ 27 | { 28 | "resources": ["camera.html", "camera.js", "video.html", "video.js"], 29 | "matches": ["https://*/*", "http://*/*"] 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /offscreen.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /offscreen.js: -------------------------------------------------------------------------------- 1 | // listen for messages from the service worker 2 | chrome.runtime.onMessage.addListener((message, sender) => { 3 | console.log("[offscreen] message received", message, sender); 4 | 5 | switch (message.type) { 6 | case "start-recording": 7 | console.log("offscreen start recording tab"); 8 | startRecording(message.data); 9 | break; 10 | case "stop-recording": 11 | console.log("stop recording tab"); 12 | stopRecording(); 13 | break; 14 | default: 15 | console.log("default"); 16 | } 17 | 18 | return true; 19 | }); 20 | 21 | let recorder; 22 | let data = []; 23 | 24 | async function stopRecording() { 25 | console.log("stop recording"); 26 | 27 | if (recorder?.state === "recording") { 28 | recorder.stop(); 29 | 30 | // stop all streams 31 | recorder.stream.getTracks().forEach((t) => t.stop()); 32 | } 33 | } 34 | 35 | async function startRecording(streamId) { 36 | try { 37 | if (recorder?.state === "recording") { 38 | throw new Error("Called startRecording while recording is in progress."); 39 | } 40 | 41 | console.log("start recording", streamId); 42 | 43 | // use the tabCaptured streamId 44 | const media = await navigator.mediaDevices.getUserMedia({ 45 | audio: { 46 | mandatory: { 47 | chromeMediaSource: "tab", 48 | chromeMediaSourceId: streamId, 49 | }, 50 | }, 51 | video: { 52 | mandatory: { 53 | chromeMediaSource: "tab", 54 | chromeMediaSourceId: streamId, 55 | }, 56 | }, 57 | }); 58 | 59 | // get microphone audio 60 | const microphone = await navigator.mediaDevices.getUserMedia({ 61 | audio: { echoCancellation: false }, 62 | }); 63 | // combine the streams 64 | const mixedContext = new AudioContext(); 65 | const mixedDest = mixedContext.createMediaStreamDestination(); 66 | 67 | mixedContext.createMediaStreamSource(microphone).connect(mixedDest); 68 | mixedContext.createMediaStreamSource(media).connect(mixedDest); 69 | 70 | const combinedStream = new MediaStream([ 71 | media.getVideoTracks()[0], 72 | mixedDest.stream.getTracks()[0], 73 | ]); 74 | 75 | recorder = new MediaRecorder(combinedStream, { mimeType: "video/webm" }); 76 | 77 | // listen for data 78 | recorder.ondataavailable = (event) => { 79 | console.log("data available", event); 80 | data.push(event.data); 81 | }; 82 | 83 | // listen for when recording stops 84 | recorder.onstop = async () => { 85 | console.log("recording stopped"); 86 | // send the data to the service worker 87 | console.log("sending data to service worker"); 88 | 89 | // convert this into a blog and open window 90 | const blob = new Blob(data, { type: "video/webm" }); 91 | const url = URL.createObjectURL(blob); 92 | // send message to service worker to open tab 93 | chrome.runtime.sendMessage({ type: "open-tab", url }); 94 | }; 95 | 96 | // start recording 97 | recorder.start(); 98 | } catch (err) { 99 | console.log("error", err); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /popup.html: -------------------------------------------------------------------------------- 1 | 16 | 17 |