├── 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 |
18 |

Record options

19 | 20 |
21 | 22 | 23 |
24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /popup.js: -------------------------------------------------------------------------------- 1 | const recordTab = document.querySelector("#tab"); 2 | const recordScreen = document.querySelector("#screen"); 3 | 4 | const injectCamera = async () => { 5 | // inject the content script into the current page 6 | const tab = await chrome.tabs.query({ active: true, currentWindow: true }); 7 | if (!tab) return; 8 | 9 | const tabId = tab[0].id; 10 | console.log("inject into tab", tabId); 11 | await chrome.scripting.executeScript({ 12 | // content.js is the file that will be injected 13 | files: ["content.js"], 14 | target: { tabId }, 15 | }); 16 | }; 17 | 18 | const removeCamera = async () => { 19 | // inject the content script into the current page 20 | const tab = await chrome.tabs.query({ active: true, currentWindow: true }); 21 | if (!tab) return; 22 | 23 | const tabId = tab[0].id; 24 | console.log("inject into tab", tabId); 25 | await chrome.scripting.executeScript({ 26 | // content.js is the file that will be injected 27 | func: () => { 28 | const camera = document.querySelector("#rusty-camera"); 29 | if (!camera) return; 30 | document.querySelector("#rusty-camera").style.display = "none"; 31 | }, 32 | target: { tabId }, 33 | }); 34 | }; 35 | 36 | // check chrome storage if recording is on 37 | const checkRecording = async () => { 38 | const recording = await chrome.storage.local.get(["recording", "type"]); 39 | const recordingStatus = recording.recording || false; 40 | const recordingType = recording.type || ""; 41 | console.log("recording status", recordingStatus, recordingType); 42 | return [recordingStatus, recordingType]; 43 | }; 44 | 45 | const init = async () => { 46 | const recordingState = await checkRecording(); 47 | 48 | console.log("recording state", recordingState); 49 | 50 | if (recordingState[0] === true) { 51 | if (recordingState[1] === "tab") { 52 | recordTab.innerText = "Stop Recording"; 53 | } else { 54 | recordScreen.innerText = "Stop Recording"; 55 | } 56 | } 57 | 58 | const updateRecording = async (type) => { 59 | console.log("start recording", type); 60 | 61 | const recordingState = await checkRecording(); 62 | 63 | if (recordingState[0] === true) { 64 | // stop recording 65 | chrome.runtime.sendMessage({ type: "stop-recording" }); 66 | removeCamera(); 67 | } else { 68 | // send message to service worker to start recording 69 | chrome.runtime.sendMessage({ 70 | type: "start-recording", 71 | recordingType: type, 72 | }); 73 | injectCamera(); 74 | } 75 | 76 | // close popup 77 | window.close(); 78 | }; 79 | 80 | recordTab.addEventListener("click", async () => { 81 | console.log("updateRecording tab clicked"); 82 | updateRecording("tab"); 83 | }); 84 | 85 | recordScreen.addEventListener("click", async () => { 86 | console.log("updateRecording screen clicked"); 87 | updateRecording("screen"); 88 | }); 89 | }; 90 | 91 | init(); 92 | -------------------------------------------------------------------------------- /prettierrc.yaml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustyzone/screen-rec/aa30048acea6724a8eac779f73eb3cb39f1131d6/prettierrc.yaml -------------------------------------------------------------------------------- /service-worker.js: -------------------------------------------------------------------------------- 1 | // check 2 | const checkRecording = async () => { 3 | const recording = await chrome.storage.local.get(["recording", "type"]); 4 | const recordingStatus = recording.recording || false; 5 | const recordingType = recording.type || ""; 6 | console.log("recording status", recordingStatus, recordingType); 7 | return [recordingStatus, recordingType]; 8 | }; 9 | 10 | // update recording state 11 | const updateRecording = async (state, type) => { 12 | console.log("update recording", type); 13 | chrome.storage.local.set({ recording: state, type }); 14 | }; 15 | 16 | const injectCamera = async () => { 17 | // inject the content script into the current page 18 | const tab = await chrome.tabs.query({ active: true, currentWindow: true }); 19 | if (!tab) return; 20 | 21 | const tabId = tab[0].id; 22 | console.log("inject into tab", tabId); 23 | await chrome.scripting.executeScript({ 24 | // content.js is the file that will be injected 25 | files: ["content.js"], 26 | target: { tabId }, 27 | }); 28 | }; 29 | 30 | const removeCamera = async () => { 31 | // inject the content script into the current page 32 | const tab = await chrome.tabs.query({ active: true, currentWindow: true }); 33 | if (!tab) return; 34 | 35 | const tabId = tab[0].id; 36 | console.log("inject into tab", tabId); 37 | await chrome.scripting.executeScript({ 38 | // content.js is the file that will be injected 39 | func: () => { 40 | const camera = document.querySelector("#rusty-camera"); 41 | if (!camera) return; 42 | document.querySelector("#rusty-camera").style.display = "none"; 43 | }, 44 | target: { tabId }, 45 | }); 46 | }; 47 | 48 | // listen for changes to the focused / current tab 49 | chrome.tabs.onActivated.addListener(async (activeInfo) => { 50 | console.log("tab activated", activeInfo); 51 | 52 | // grab the tab 53 | const activeTab = await chrome.tabs.get(activeInfo.tabId); 54 | if (!activeTab) return; 55 | const tabUrl = activeTab.url; 56 | 57 | // if chrome or extension page, return 58 | if ( 59 | tabUrl.startsWith("chrome://") || 60 | tabUrl.startsWith("chrome-extension://") 61 | ) { 62 | console.log("chrome or extension page - exiting"); 63 | return; 64 | } 65 | 66 | // check if we are recording & if we are recording the scren 67 | const [recording, recordingType] = await checkRecording(); 68 | 69 | console.log("recording check after tab change", { 70 | recording, 71 | recordingType, 72 | tabUrl, 73 | }); 74 | 75 | if (recording && recordingType === "screen") { 76 | // inject the camera 77 | injectCamera(); 78 | } else { 79 | // remove the camera 80 | removeCamera(); 81 | } 82 | }); 83 | 84 | const startRecording = async (type) => { 85 | console.log("start recording", type); 86 | const currentstate = await checkRecording(); 87 | console.log("current state", currentstate); 88 | updateRecording(true, type); 89 | // update the icon 90 | chrome.action.setIcon({ path: "icons/recording.png" }); 91 | if (type === "tab") { 92 | recordTabState(true); 93 | } 94 | if (type === "screen") { 95 | recordScreen(); 96 | } 97 | }; 98 | 99 | const stopRecording = async () => { 100 | console.log("stop recording"); 101 | updateRecording(false, ""); 102 | // update the icon 103 | chrome.action.setIcon({ path: "icons/not-recording.png" }); 104 | recordTabState(false); 105 | }; 106 | 107 | const recordScreen = async () => { 108 | // create a pinned focused tab - with an index of 0 109 | const desktopRecordPath = chrome.runtime.getURL("desktopRecord.html"); 110 | 111 | const currentTab = await chrome.tabs.query({ 112 | active: true, 113 | currentWindow: true, 114 | }); 115 | const currentTabId = currentTab[0].id; 116 | 117 | const newTab = await chrome.tabs.create({ 118 | url: desktopRecordPath, 119 | pinned: true, 120 | active: true, 121 | index: 0, 122 | }); 123 | 124 | // wait for 500ms send a message to the tab to start recording 125 | setTimeout(() => { 126 | chrome.tabs.sendMessage(newTab.id, { 127 | type: "start-recording", 128 | focusedTabId: currentTabId, 129 | }); 130 | }, 500); 131 | }; 132 | 133 | const recordTabState = async (start = true) => { 134 | // setup our offscrene document 135 | const existingContexts = await chrome.runtime.getContexts({}); 136 | const offscreenDocument = existingContexts.find( 137 | (c) => c.contextType === "OFFSCREEN_DOCUMENT" 138 | ); 139 | 140 | // If an offscreen document is not already open, create one. 141 | if (!offscreenDocument) { 142 | // Create an offscreen document. 143 | await chrome.offscreen.createDocument({ 144 | url: "offscreen.html", 145 | reasons: ["USER_MEDIA", "DISPLAY_MEDIA"], 146 | justification: "Recording from chrome.tabCapture API", 147 | }); 148 | } 149 | 150 | if (start) { 151 | // use the tapCapture API to get the stream 152 | // get the id of the active tab 153 | const tab = await chrome.tabs.query({ active: true, currentWindow: true }); 154 | if (!tab) return; 155 | 156 | const tabId = tab[0].id; 157 | 158 | console.log("tab id", tabId); 159 | 160 | const streamId = await chrome.tabCapture.getMediaStreamId({ 161 | targetTabId: tabId, 162 | }); 163 | 164 | console.log("stream id", streamId); 165 | 166 | // send this to our offscreen document 167 | chrome.runtime.sendMessage({ 168 | type: "start-recording", 169 | target: "offscreen", 170 | data: streamId, 171 | }); 172 | } else { 173 | // stop 174 | chrome.runtime.sendMessage({ 175 | type: "stop-recording", 176 | target: "offscreen", 177 | }); 178 | } 179 | }; 180 | 181 | const openTabWithVideo = async (message) => { 182 | console.log("request to open tab with video", message); 183 | 184 | // that message will either have a url or base64 encoded video 185 | const { url: videoUrl, base64 } = message; 186 | 187 | if (!videoUrl && !base64) return; 188 | 189 | // open tab 190 | const url = chrome.runtime.getURL("video.html"); 191 | const newTab = await chrome.tabs.create({ url }); 192 | 193 | // send message to tab 194 | setTimeout(() => { 195 | chrome.tabs.sendMessage(newTab.id, { 196 | type: "play-video", 197 | videoUrl, 198 | base64, 199 | }); 200 | }, 500); 201 | }; 202 | 203 | // add listender for messages 204 | chrome.runtime.onMessage.addListener(function (request, sender) { 205 | console.log("message received", request, sender); 206 | 207 | switch (request.type) { 208 | case "open-tab": 209 | openTabWithVideo(request); 210 | break; 211 | case "start-recording": 212 | startRecording(request.recordingType); 213 | break; 214 | case "stop-recording": 215 | stopRecording(); 216 | break; 217 | default: 218 | console.log("default"); 219 | } 220 | 221 | return true; 222 | }); 223 | -------------------------------------------------------------------------------- /video.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Video Player 7 | 8 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /video.js: -------------------------------------------------------------------------------- 1 | // to save the video to local storage 2 | const saveVideo = (videoUrl) => { 3 | chrome.storage.local.set({ videoUrl }); 4 | }; 5 | 6 | // on page open, check if there is a video url 7 | chrome.storage.local.get(["videoUrl"], (result) => { 8 | console.log("video url", result); 9 | if (result.videoUrl) { 10 | console.log("play video from storage", result); 11 | playVideo(result); 12 | } 13 | }); 14 | 15 | const playVideo = (message) => { 16 | const videoElement = document.querySelector("#recorded-video"); 17 | 18 | const url = message?.videoUrl || message?.base64; 19 | // update the saved video url 20 | saveVideo(url); 21 | 22 | videoElement.src = url; 23 | videoElement.play(); 24 | }; 25 | 26 | chrome.runtime.onMessage.addListener((message, sender) => { 27 | switch (message.type) { 28 | case "play-video": 29 | console.log("play video", message); 30 | playVideo(message); 31 | break; 32 | default: 33 | console.log("default"); 34 | } 35 | }); 36 | --------------------------------------------------------------------------------