Skip to content

Instantly share code, notes, and snippets.

@netham45
Created September 11, 2024 22:39
Show Gist options
  • Save netham45/d7432f1bff3143f3ea181aaf58d7a654 to your computer and use it in GitHub Desktop.
Save netham45/d7432f1bff3143f3ea181aaf58d7a654 to your computer and use it in GitHub Desktop.
const videoElement = document.getElementsByTagName('video')[0];
let isRecording = false;
let audioContext, source, analyser, scriptProcessor;
let canvas, ctx;
let currentFingerprint = [];
let storedFingerprint = null;
let lastCheckTime = 0;
const SAMPLE_RATE = 22050;
const FFT_SIZE = 1024;
const MIN_FREQ = 300;
const MAX_FREQ = 2000;
const FINGERPRINT_DURATION = 5; // seconds
const CHECK_INTERVAL = 1; // seconds
const FINGERPRINT_LENGTH = Math.floor(SAMPLE_RATE * FINGERPRINT_DURATION / (FFT_SIZE / 2));
function setupCanvas() {
canvas = document.createElement('canvas');
canvas.width = FINGERPRINT_LENGTH;
canvas.height = 32;
ctx = canvas.getContext('2d');
canvas.style.position = "absolute";
canvas.style.zIndex = 9999;
canvas.style.top = "10px";
canvas.style.left = "10px";
canvas.style.border = "1px solid white";
canvas.style.backgroundColor = "black";
document.body.insertBefore(canvas, document.body.firstChild);
}
function startRecording(videoElement) {
if (isRecording) return;
isRecording = true;
if (!canvas) setupCanvas();
try {
audioContext = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: SAMPLE_RATE });
} catch (e) {
console.error('Web Audio API is not supported in this browser', e);
return;
}
source = audioContext.createMediaElementSource(videoElement);
analyser = audioContext.createAnalyser();
scriptProcessor = audioContext.createScriptProcessor(FFT_SIZE, 1, 1);
analyser.fftSize = FFT_SIZE;
analyser.minDecibels = -90;
analyser.maxDecibels = -30;
analyser.smoothingTimeConstant = 0;
source.connect(analyser);
source.connect(audioContext.destination);
analyser.connect(scriptProcessor);
scriptProcessor.connect(audioContext.destination);
const bufferLength = analyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
const minBin = Math.floor(MIN_FREQ / (SAMPLE_RATE / FFT_SIZE));
const maxBin = Math.ceil(MAX_FREQ / (SAMPLE_RATE / FFT_SIZE));
scriptProcessor.onaudioprocess = function() {
analyser.getByteFrequencyData(dataArray);
let columnFingerprint = new Array(32).fill(0);
for (let i = minBin; i < maxBin; i++) {
const y = Math.floor((i - minBin) / ((maxBin - minBin) / 32));
columnFingerprint[y] = dataArray[i] > 128 ? 1 : 0;
}
currentFingerprint.push(columnFingerprint);
if (currentFingerprint.length > FINGERPRINT_LENGTH) {
currentFingerprint.shift();
}
renderFingerprint();
if (storedFingerprint && audioContext.currentTime - lastCheckTime >= CHECK_INTERVAL) {
checkForMatch();
lastCheckTime = audioContext.currentTime;
}
};
videoElement.play();
}
function renderFingerprint() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (let x = 0; x < currentFingerprint.length; x++) {
for (let y = 0; y < 32; y++) {
if (currentFingerprint[x][y] === 1) {
ctx.fillStyle = 'white';
ctx.fillRect(x, y, 1, 1);
}
}
}
}
function stopRecording() {
if (!isRecording) return;
isRecording = false;
scriptProcessor.disconnect();
analyser.disconnect();
source.disconnect();
}
function captureFiveSeconds() {
if (currentFingerprint.length < FINGERPRINT_LENGTH) {
console.log(`Not enough data to capture five seconds. Current length: ${currentFingerprint.length}, Required: ${FINGERPRINT_LENGTH}`);
return null;
}
storedFingerprint = currentFingerprint.slice();
localStorage.setItem('storedFingerprint', JSON.stringify(storedFingerprint));
console.log("Five second fingerprint captured and stored in localStorage");
return storedFingerprint;
}
function checkForMatch() {
if (!storedFingerprint || currentFingerprint.length < FINGERPRINT_LENGTH) return;
const lastSecond = currentFingerprint.slice(-Math.floor(FINGERPRINT_LENGTH / 5));
for (let i = 0; i <= storedFingerprint.length - lastSecond.length; i++) {
const similarity = calculateSimilarity(storedFingerprint.slice(i, i + lastSecond.length), lastSecond);
if (similarity > 0.9) {
console.log(`Match found at position ${i / (FINGERPRINT_LENGTH / FINGERPRINT_DURATION)} seconds with ${similarity.toFixed(2)} similarity`);
onMatch(i, similarity);
return;
}
}
}
function calculateSimilarity(fingerprint1, fingerprint2) {
let matchingPixels = 0;
let totalPixels = fingerprint1.length * fingerprint1[0].length;
for (let i = 0; i < fingerprint1.length; i++) {
for (let j = 0; j < fingerprint1[i].length; j++) {
if (fingerprint1[i][j] === fingerprint2[i][j]) {
matchingPixels++;
}
}
}
return matchingPixels / totalPixels;
}
function skipForward(seconds) {
if (videoElement && !isNaN(seconds) && videoElement.readyState >= 2) {
const newTime = videoElement.currentTime + seconds;
videoElement.currentTime = Math.min(newTime, videoElement.duration);
} else {
console.warn('Video not ready or invalid skip time');
}
}
function onMatch(position, similarity) {
let jump = 5 - position / (FINGERPRINT_LENGTH / FINGERPRINT_DURATION);
console.log(`Match found! The current audio matches the stored fingerprint at position ${position / (FINGERPRINT_LENGTH / FINGERPRINT_DURATION)} seconds with ${similarity.toFixed(2)} similarity`);
canvas.style.border = "3px solid red";
setTimeout(() => { canvas.style.border = "1px solid white"; }, 1000);
skipForward(jump);
}
function startAudioProcessing() {
if (!videoElement) {
console.error('Video element not found');
return;
}
const storedFingerprintString = localStorage.getItem('storedFingerprint');
if (storedFingerprintString) {
storedFingerprint = JSON.parse(storedFingerprintString);
console.log('Fingerprint loaded from localStorage');
}
startRecording(videoElement);
const btnTag = document.createElement("button");
btnTag.textContent = "Tag Last 5s of audio to be skipped";
btnTag.style.position = "absolute";
btnTag.style.zIndex = "99999";
btnTag.style.top = "50px";
btnTag.style.left = "10px";
btnTag.style.padding = "10px";
btnTag.style.backgroundColor = "#4CAF50";
btnTag.style.color = "white";
btnTag.style.border = "none";
btnTag.style.cursor = "pointer";
btnTag.addEventListener('click', captureFiveSeconds);
document.body.appendChild(btnTag);
videoElement.addEventListener('ended', stopRecording);
}
function exportFingerprintAsBase64() {
if (!storedFingerprint) {
console.log("No fingerprint stored to export");
return null;
}
// Convert the fingerprint to a JSON string
const fingerprintJSON = JSON.stringify(storedFingerprint);
// Convert the JSON string to a Uint8Array
const uint8Array = new TextEncoder().encode(fingerprintJSON);
// Convert the Uint8Array to base64
const base64String = btoa(String.fromCharCode.apply(null, uint8Array));
console.log("Fingerprint exported as base64");
return base64String;
}
function importFingerprintFromBase64(base64String) {
if (!base64String) {
console.log("No base64 string provided");
return false;
}
try {
// Convert base64 to Uint8Array
const uint8Array = new Uint8Array(atob(base64String).split('').map(char => char.charCodeAt(0)));
// Convert Uint8Array to JSON string
const fingerprintJSON = new TextDecoder().decode(uint8Array);
// Parse JSON string to get the fingerprint
const importedFingerprint = JSON.parse(fingerprintJSON);
// Validate the imported fingerprint structure
if (!Array.isArray(importedFingerprint) || importedFingerprint.length !== FINGERPRINT_LENGTH) {
throw new Error("Invalid fingerprint structure");
}
// Store the imported fingerprint
storedFingerprint = importedFingerprint;
localStorage.setItem('storedFingerprint', JSON.stringify(storedFingerprint));
console.log("Fingerprint imported successfully");
return true;
} catch (error) {
console.error("Error importing fingerprint:", error);
return false;
}
}
startAudioProcessing();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment