diff --git a/public/manifest.json b/public/manifest.json index 9559059..3362b54 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -5,11 +5,11 @@ "version": "0.11.0", "manifest_version": 3, "icons": { - "16": "./static/logo/Logo_16.png", - "32": "./static/logo/Logo_32.png", - "48": "./static/logo/Logo_48.png", - "128": "./static/logo/Logo_128.png", - "256": "./static/logo/Logo_256.png" + "16": "./static/Logo_16.png", + "32": "./static/Logo_32.png", + "48": "./static/Logo_48.png", + "128": "./static/Logo_128.png", + "256": "./static/Logo_256.png" }, "action": { "default_popup": "./popup.html" diff --git a/src/ui/assets/Background.svg b/public/static/Background.svg similarity index 100% rename from src/ui/assets/Background.svg rename to public/static/Background.svg diff --git a/public/static/logo/Logo_128.png b/public/static/Logo_128.png similarity index 100% rename from public/static/logo/Logo_128.png rename to public/static/Logo_128.png diff --git a/public/static/logo/Logo_16.png b/public/static/Logo_16.png similarity index 100% rename from public/static/logo/Logo_16.png rename to public/static/Logo_16.png diff --git a/public/static/logo/Logo_256.png b/public/static/Logo_256.png similarity index 100% rename from public/static/logo/Logo_256.png rename to public/static/Logo_256.png diff --git a/public/static/logo/Logo_32.png b/public/static/Logo_32.png similarity index 100% rename from public/static/logo/Logo_32.png rename to public/static/Logo_32.png diff --git a/public/static/logo/Logo_48.png b/public/static/Logo_48.png similarity index 100% rename from public/static/logo/Logo_48.png rename to public/static/Logo_48.png diff --git a/src/ui/assets/Lookup_128.png b/public/static/Lookup_128.png similarity index 100% rename from src/ui/assets/Lookup_128.png rename to public/static/Lookup_128.png diff --git a/src/app/messaging.ts b/src/app/messaging.ts index 436f009..79716c6 100644 --- a/src/app/messaging.ts +++ b/src/app/messaging.ts @@ -3,7 +3,7 @@ export enum MessageType { CameraBubbleShow = "CameraBubbleShow", CameraBubbleHide = "CameraBubbleHide", RecordingStart = "RecordingStart", - RecordingStop = "RecordingStop", + RecordingComplete = "RecordingComplete", RecordingPause = "RecordingPause", RecordingResume = "RecordingResume", RecordingCancel = "RecordingCancel", @@ -65,9 +65,9 @@ export const sender = { target: "background", }); }, - recordingStop: () => { + recordingComplete: () => { return chrome.runtime.sendMessage({ - type: MessageType.RecordingStop, + type: MessageType.RecordingComplete, target: "background", }); }, diff --git a/src/app/storage.ts b/src/app/storage.ts index 11ca8f0..b0ab1f7 100644 --- a/src/app/storage.ts +++ b/src/app/storage.ts @@ -9,6 +9,7 @@ export enum StorageKey { DevicesVideoId = "devices.video.id", DevicesVideoName = "devices.video.name", RecordingState = "recording.state", + RecordingDownloadId = "recording.download_id", UiCameraBubbleEnabled = "ui.cameraBubble.enabled", UiCameraBubblePosition = "ui.cameraBubble.position", UiCameraBubbleSize = "ui.cameraBubble.size", @@ -18,6 +19,8 @@ export enum StorageKey { export enum RecordingState { NotStarted = "NotStarted", InProgress = "InProgress", + Completed = "Completed", + Downloading = "Downloading", OnPause = "OnPause", } @@ -32,6 +35,7 @@ type StorageValueTypeMap = { [StorageKey.DevicesVideoId]: string; [StorageKey.DevicesVideoName]: string; [StorageKey.RecordingState]: RecordingState; + [StorageKey.RecordingDownloadId]: number; [StorageKey.UiCameraBubbleEnabled]: boolean; [StorageKey.UiCameraBubblePosition]: { x: number; y: number }; [StorageKey.UiCameraBubbleSize]: { width: number; height: number }; @@ -83,6 +87,7 @@ export const storage = { }, recording: { state: createStorageSetterGetter(StorageKey.RecordingState), + downloadId: createStorageSetterGetter(StorageKey.RecordingDownloadId), }, ui: { cameraBubble: { diff --git a/src/background/controllers/recorder_controller.ts b/src/background/controllers/recorder_controller.ts index aad1b59..e4a4bbe 100644 --- a/src/background/controllers/recorder_controller.ts +++ b/src/background/controllers/recorder_controller.ts @@ -13,6 +13,7 @@ class Recorder { #chunks: BlobPart[]; #isRecordingCanceled: boolean; #mediaRecorder?: MediaRecorder; + #downloadUrl?: string; constructor(mimeType: string) { console.log(`Recorder.constructor(mimeType='${mimeType}')`); @@ -24,7 +25,7 @@ class Recorder { #onTrackEnded() { (async () => { - await sender.background.recordingStop(); + await sender.background.recordingComplete(); })().catch((err) => { console.error( `Recorder.#onTrackEnded error: ${(err as Error).toString()}`, @@ -48,17 +49,15 @@ class Recorder { return; } - const downloadUrl = URL.createObjectURL( + this.#downloadUrl = URL.createObjectURL( new Blob(this.#chunks, { type: event.data.type, }), ); await sender.background.recordingSave({ - recordingUrl: downloadUrl, + recordingUrl: this.#downloadUrl, }); - - URL.revokeObjectURL(downloadUrl); })().catch((err) => { console.error( `Recorder.#onMediaRecorderDataAvailable error: ${(err as Error).toString()}`, @@ -78,7 +77,7 @@ class Recorder { }); for (const track of this.#mediaStream.getTracks()) { - track.addEventListener("ended", this.#onTrackEnded.bind(this)); + track.addEventListener("ended", this.#onTrackEnded); } this.#mediaRecorder.addEventListener( @@ -95,6 +94,7 @@ class Recorder { } this.#mediaRecorder.stop(); for (const track of this.#mediaStream.getTracks()) { + track.removeEventListener("ended", this.#onTrackEnded); track.stop(); } } @@ -120,6 +120,14 @@ class Recorder { this.#isRecordingCanceled = true; this.stop(); } + + cleanup() { + if (!this.#downloadUrl) { + return; + } + URL.revokeObjectURL(this.#downloadUrl); + this.#downloadUrl = ""; + } } class RecorderController { @@ -207,6 +215,7 @@ class RecorderController { console.error("Recorder is not created"); return; } + RecorderController.#recorder.cleanup(); RecorderController.#recorder = undefined; } } diff --git a/src/background/controllers/recording_controller.ts b/src/background/controllers/recording_controller.ts index 77bc71a..0bfa4d0 100644 --- a/src/background/controllers/recording_controller.ts +++ b/src/background/controllers/recording_controller.ts @@ -40,15 +40,13 @@ class RecordingController { await storage.recording.state.set(RecordingState.InProgress); } - static async stop() { - console.log("RecordingController.stop()"); + static async complete() { + console.log("RecordingController.complete()"); if (!(await chrome.offscreen.hasDocument())) { return; } await sender.offscreen.recorderStop(); - await sender.offscreen.recorderDelete(); - await chrome.offscreen.closeDocument(); - await storage.recording.state.set(RecordingState.NotStarted); + await storage.recording.state.set(RecordingState.Completed); } static async pause() { @@ -85,9 +83,24 @@ class RecordingController { if (!(await chrome.offscreen.hasDocument())) { return; } - await chrome.downloads.download({ + const downloadId = await chrome.downloads.download({ url: options.recordingUrl, }); + await storage.recording.downloadId.set(downloadId); + await storage.recording.state.set(RecordingState.Downloading); + console.log( + `RecordingController.save(): Downloading started with id=${downloadId}`, + ); + } + + static async saveComplete() { + console.log("RecordingController.saveComplete()"); + if (!(await chrome.offscreen.hasDocument())) { + return; + } + await sender.offscreen.recorderDelete(); + await storage.recording.state.set(RecordingState.NotStarted); + await storage.recording.downloadId.set(0); } } @@ -106,8 +119,8 @@ chrome.runtime.onMessage.addListener( case MessageType.RecordingStart: await RecordingController.start(); break; - case MessageType.RecordingStop: - await RecordingController.stop(); + case MessageType.RecordingComplete: + await RecordingController.complete(); break; case MessageType.RecordingPause: await RecordingController.pause(); @@ -141,3 +154,29 @@ chrome.runtime.onMessage.addListener( return true; }, ); + +chrome.downloads.onChanged.addListener((downloadDelta) => { + (async () => { + const downloadId = await storage.recording.downloadId.get(); + if (!downloadId || downloadDelta.id !== downloadId) { + return; + } + const { state } = downloadDelta; + // NOTE: We may receive filename update here without `state` on downloading, + // so we can just ignore such case + if (!state) { + return; + } + if (state?.current === "complete") { + await RecordingController.saveComplete(); + return; + } + console.error( + `Problem with recording downloading: ${JSON.stringify(downloadDelta)}`, + ); + })().catch((err) => { + console.error( + `Error in 'chrome.downloads.onChanged' handler: ${(err as Error).toString()}`, + ); + }); +}); diff --git a/src/ui/pages/popup/Popup.tsx b/src/ui/pages/popup/Popup.tsx index 229c82e..c55f4a6 100644 --- a/src/ui/pages/popup/Popup.tsx +++ b/src/ui/pages/popup/Popup.tsx @@ -14,8 +14,8 @@ import { IconTrash, IconUserCircle, } from "@tabler/icons-react"; -import background from "/assets/Background.svg"; -import lookup128 from "/assets/Lookup_128.png"; +import background from "/static/Background.svg"; +import lookup128 from "/static/Lookup_128.png"; const Settings = () => { const [cameraBubbleEnabled] = useStorageValue( @@ -119,13 +119,13 @@ const RecordingControl = () => { className="h-[28px] w-[28px] stroke-black transition-colors hover:stroke-[#00d492]" stroke={2} onClick={() => { - sender.background - .recordingStop() - .catch((err) => - console.error( - `Can't stop recording: ${(err as Error).toString()}`, - ), - ); + (async () => { + await sender.background.recordingComplete(); + })().catch((err) => + console.error( + `Can't complete and download recording: ${(err as Error).toString()}`, + ), + ); }} />