File size: 8,139 Bytes
0f27de7
3337c5d
4c0afe5
3337c5d
 
 
 
 
 
0f27de7
 
25b4391
7902802
3337c5d
0f27de7
4c0afe5
0f27de7
 
4c0afe5
3337c5d
4c0afe5
0f27de7
3337c5d
 
 
 
 
0f27de7
 
4c0afe5
 
 
0f27de7
3337c5d
 
 
0f27de7
 
4c0afe5
0f27de7
6c71866
4c0afe5
3337c5d
4c0afe5
 
 
6c71866
0f27de7
 
3337c5d
 
 
 
 
 
 
 
0f27de7
 
 
 
 
 
 
 
3337c5d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0f27de7
4c0afe5
3337c5d
 
 
 
 
 
 
 
 
4c0afe5
0f27de7
 
4c0afe5
 
 
0f27de7
3337c5d
 
 
 
 
 
 
 
 
 
4c0afe5
3337c5d
4c0afe5
3337c5d
 
 
 
 
 
4c0afe5
6c71866
 
3337c5d
 
 
 
 
 
 
4c0afe5
3337c5d
4c0afe5
0f27de7
 
3337c5d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4c0afe5
3337c5d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4c0afe5
 
0f27de7
 
 
4c0afe5
0f27de7
 
 
 
 
 
3337c5d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
/**
 * Authrix Extension β€” Background Service Worker v3
 *
 * Architecture (MV3-compliant):
 *   1. tabCapture.getMediaStreamId()  β†’ get stream ID for the target tab
 *   2. Create offscreen document      β†’ only place getUserMedia(tab) works in MV3
 *   3. offscreen.js records the stream and sends BLOB_READY back here
 *   4. We POST the blob to the local FastAPI backend
 *   5. Result is sent to the content script overlay on the original tab
 */

const API_BASE    = 'https://aarav13-authrix.hf.space';
const CAPTURE_SEC = 8;   // Reduced from 20s β€” 8s gives enough frames for accurate detection
const OFFSCREEN_URL = chrome.runtime.getURL('offscreen.html');

// ── Context menu ──────────────────────────────────────────────────────────────
chrome.runtime.onInstalled.addListener(() => {
  chrome.contextMenus.create({
    id:       'authrix-capture',
    title:    'πŸ” Analyze with Authrix',
    contexts: ['page', 'video', 'frame'],
  });
  chrome.contextMenus.create({
    id:       'authrix-url',
    title:    'πŸ”— Analyze video URL with Authrix',
    contexts: ['link'],
  });
});

chrome.contextMenus.onClicked.addListener((info, tab) => {
  if (info.menuItemId === 'authrix-capture' && tab?.id) {
    startCapture(tab.id);
  }
  if (info.menuItemId === 'authrix-url' && tab?.id && info.linkUrl) {
    analyzeUrl(info.linkUrl, tab.id);
  }
});

// ── Message handler ───────────────────────────────────────────────────────────
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {

  if (msg.type === 'START_CAPTURE') {
    const tabId = msg.tabId || sender.tab?.id;
    startCapture(tabId)
      .then(() => sendResponse({ ok: true }))
      .catch(e => sendResponse({ ok: false, error: e.message }));
    return true;
  }

  if (msg.type === 'ANALYZE_URL') {
    const tabId = msg.tabId || sender.tab?.id;
    analyzeUrl(msg.url, tabId)
      .then(() => sendResponse({ ok: true }))
      .catch(e => sendResponse({ ok: false, error: e.message }));
    return true;
  }

  if (msg.type === 'CHECK_HEALTH') {
    fetch(`${API_BASE}/health`)
      .then(r => r.json())
      .then(d => sendResponse({ ok: true, data: d }))
      .catch(() => sendResponse({ ok: false }));
    return true;
  }

  // ── Messages from offscreen document ─────────────────────────────────────
  if (msg.type === 'BLOB_READY') {
    handleBlobReady(msg.chunks, msg.mimeType, msg.tabId, msg.totalSize);
    return false;
  }

  if (msg.type === 'RECORD_PROGRESS') {
    if (msg.tabId) {
      chrome.tabs.sendMessage(msg.tabId, {
        type: 'CAPTURE_PROGRESS',
        elapsed: msg.elapsed,
        total:   msg.total,
      }).catch(() => {});
    }
    return false;
  }

  if (msg.type === 'RECORD_ERROR') {
    if (msg.tabId) {
      chrome.tabs.sendMessage(msg.tabId, {
        type:  'ANALYSIS_ERROR',
        error: msg.error,
      }).catch(() => {});
    }
    closeOffscreen();
    return false;
  }
});

// ── Start tab capture ─────────────────────────────────────────────────────────
async function startCapture(tabId) {
  if (!tabId) throw new Error('No tab ID');

  // Show loading overlay on the tab
  await chrome.tabs.sendMessage(tabId, { type: 'SHOW_CAPTURE_OVERLAY' }).catch(() => {
    // Content script may not be injected yet β€” inject it
    return chrome.scripting.executeScript({
      target: { tabId },
      files:  ['content.js'],
    }).then(() =>
      chrome.tabs.sendMessage(tabId, { type: 'SHOW_CAPTURE_OVERLAY' })
    );
  });

  // Get stream ID (must be called from background, not offscreen)
  const streamId = await new Promise((resolve, reject) => {
    chrome.tabCapture.getMediaStreamId({ targetTabId: tabId }, id => {
      if (chrome.runtime.lastError) {
        reject(new Error(chrome.runtime.lastError.message));
      } else {
        resolve(id);
      }
    });
  });

  // Ensure offscreen document exists
  await ensureOffscreen();

  // Tell offscreen to start recording
  await chrome.runtime.sendMessage({
    type:       'RECORD_OFFSCREEN',
    streamId,
    durationMs: CAPTURE_SEC * 1000,
    tabId,
  });
}

// ── Analyze a direct video URL ────────────────────────────────────────────────
async function analyzeUrl(url, tabId) {
  if (!url) throw new Error('No URL provided');

  if (tabId) {
    await chrome.tabs.sendMessage(tabId, { type: 'SHOW_URL_ANALYSIS_OVERLAY', url }).catch(() => {});
  }

  try {
    const res = await fetch(`${API_BASE}/analyze-url`, {
      method:  'POST',
      headers: { 'Content-Type': 'application/json' },
      body:    JSON.stringify({ url }),
    });

    if (!res.ok) {
      const err = await res.json().catch(() => ({}));
      throw new Error(err.detail || `Backend error ${res.status}`);
    }

    const result = await res.json();
    result.file  = url.length > 60 ? url.slice(0, 57) + '…' : url;

    chrome.storage.local.set({ lastResult: result });

    if (tabId) {
      chrome.tabs.sendMessage(tabId, { type: 'ANALYSIS_RESULT', result }).catch(() => {});
    }
  } catch (err) {
    if (tabId) {
      chrome.tabs.sendMessage(tabId, { type: 'ANALYSIS_ERROR', error: err.message }).catch(() => {});
    }
    throw err;
  }
}

// ── Handle blob from offscreen ────────────────────────────────────────────────
async function handleBlobReady(chunks, mimeType, tabId, totalSize) {
  closeOffscreen();

  try {
    if (tabId) {
      chrome.tabs.sendMessage(tabId, { type: 'CAPTURE_PROGRESS', elapsed: 12, total: 12 }).catch(() => {});
    }

    const result = await submitBlob(chunks, mimeType, totalSize);
    result.file  = 'Tab capture';

    chrome.storage.local.set({ lastResult: result });

    if (tabId) {
      chrome.tabs.sendMessage(tabId, { type: 'ANALYSIS_RESULT', result }).catch(() => {});
    }
  } catch (err) {
    if (tabId) {
      chrome.tabs.sendMessage(tabId, { type: 'ANALYSIS_ERROR', error: err.message }).catch(() => {});
    }
  }
}

// ── Submit blob to backend ────────────────────────────────────────────────────
async function submitBlob(chunks, mimeType, totalSize) {
  // Reassemble chunks from array format back to Uint8Array
  const uint8Array = new Uint8Array(totalSize);
  let offset = 0;
  
  for (const chunk of chunks) {
    uint8Array.set(chunk, offset);
    offset += chunk.length;
  }

  const blob     = new Blob([uint8Array], { type: mimeType || 'video/webm' });
  const filename = `capture_${Date.now()}.webm`;

  const fd = new FormData();
  fd.append('file', blob, filename);

  const res = await fetch(`${API_BASE}/analyze`, { method: 'POST', body: fd });
  if (!res.ok) {
    const err = await res.json().catch(() => ({}));
    throw new Error(err.detail || `Backend error ${res.status}`);
  }
  return res.json();
}

// ── Offscreen document management ────────────────────────────────────────────
async function ensureOffscreen() {
  const existing = await chrome.offscreen.hasDocument().catch(() => false);
  if (!existing) {
    await chrome.offscreen.createDocument({
      url:    OFFSCREEN_URL,
      reasons: ['USER_MEDIA'],
      justification: 'Record tab video stream for deepfake analysis',
    });
  }
}

async function closeOffscreen() {
  const exists = await chrome.offscreen.hasDocument().catch(() => false);
  if (exists) {
    await chrome.offscreen.closeDocument().catch(() => {});
  }
}