Dark video detection
* Add local detection for users who are broadcasting dark (e.g. mostly or completely black) video feeds from their local device. * Every 5 seconds while the webcam is active, the average RGB color is sampled. If the average color value remains below 60 (out of 255) for two consecutive samples, the camera is stopped automatically.
This commit is contained in:
parent
fd36d09727
commit
a536862a91
166
src/App.vue
166
src/App.vue
|
@ -38,6 +38,9 @@ const configuration = {
|
||||||
const FileUploadMaxSize = 1024 * 1024 * 8; // 8 MB
|
const FileUploadMaxSize = 1024 * 1024 * 8; // 8 MB
|
||||||
const DebugChannelID = "barertc-debug";
|
const DebugChannelID = "barertc-debug";
|
||||||
|
|
||||||
|
const WebcamWidth = 640,
|
||||||
|
WebcamHeight = 480;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'BareRTC',
|
name: 'BareRTC',
|
||||||
components: {
|
components: {
|
||||||
|
@ -202,6 +205,20 @@ export default {
|
||||||
video: null,
|
video: null,
|
||||||
audio: null,
|
audio: null,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Detect dark video streams.
|
||||||
|
darkVideo: {
|
||||||
|
canvas: null, // <canvas> element to screenshot video into
|
||||||
|
ctx: null, // Canvas context2d
|
||||||
|
interval: null, // interval loop
|
||||||
|
lastImage: null, // data: uri of last screenshot taken
|
||||||
|
lastAverage: [], // last average RGB color
|
||||||
|
lastAverageColor: "rgba(255, 0, 255, 1)",
|
||||||
|
wasTooDark: false, // previous average was too dark
|
||||||
|
|
||||||
|
// Configuration thresholds: how dark is too dark? (0-255)
|
||||||
|
threshold: 60,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// Video flag constants (sync with values in messages.go)
|
// Video flag constants (sync with values in messages.go)
|
||||||
|
@ -1052,6 +1069,21 @@ export default {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DEBUGGING: print last dark video screenshot taken
|
||||||
|
if (this.message.toLowerCase().indexOf("/debug-dark-video") === 0) {
|
||||||
|
if (this.webcam.darkVideo.lastImage === null) {
|
||||||
|
this.ChatClient("There is no recent image available.");
|
||||||
|
} else {
|
||||||
|
this.ChatClient(
|
||||||
|
`Last average color of your video: ${JSON.stringify(this.webcam.darkVideo.lastAverage)} ` +
|
||||||
|
`<span style="background-color: ${this.webcam.darkVideo.lastAverageColor}">${this.webcam.darkVideo.lastAverageColor}</span>` +
|
||||||
|
`<br><img src="${this.webcam.darkVideo.lastImage}" width="160" height="120">`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.message = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// DEBUGGING: reconnect to the server
|
// DEBUGGING: reconnect to the server
|
||||||
if (this.message.toLowerCase().indexOf("/reconnect") === 0) {
|
if (this.message.toLowerCase().indexOf("/reconnect") === 0) {
|
||||||
this.resetChatClient();
|
this.resetChatClient();
|
||||||
|
@ -2057,8 +2089,8 @@ export default {
|
||||||
let mediaParams = {
|
let mediaParams = {
|
||||||
audio: true,
|
audio: true,
|
||||||
video: {
|
video: {
|
||||||
width: { max: 640 },
|
width: { max: WebcamWidth },
|
||||||
height: { max: 480 },
|
height: { max: WebcamHeight },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -2118,6 +2150,9 @@ export default {
|
||||||
if (changeCamera) {
|
if (changeCamera) {
|
||||||
this.updateWebRTCStreams();
|
this.updateWebRTCStreams();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Begin dark video detection.
|
||||||
|
this.initDarkVideoDetection();
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
this.ChatClient(`Webcam error: ${err}`);
|
this.ChatClient(`Webcam error: ${err}`);
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
|
@ -2605,6 +2640,8 @@ export default {
|
||||||
|
|
||||||
// Stop broadcasting.
|
// Stop broadcasting.
|
||||||
stopVideo() {
|
stopVideo() {
|
||||||
|
this.stopDarkVideoDetection();
|
||||||
|
|
||||||
// Close all WebRTC sessions.
|
// Close all WebRTC sessions.
|
||||||
for (let username of Object.keys(this.WebRTC.pc)) {
|
for (let username of Object.keys(this.WebRTC.pc)) {
|
||||||
this.closeVideo(username, "answerer");
|
this.closeVideo(username, "answerer");
|
||||||
|
@ -2771,6 +2808,128 @@ export default {
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Dark video detection.
|
||||||
|
initDarkVideoDetection() {
|
||||||
|
if (this.webcam.darkVideo.canvas === null) {
|
||||||
|
let canvas = document.createElement("canvas"),
|
||||||
|
ctx = canvas.getContext('2d');
|
||||||
|
canvas.width = WebcamWidth;
|
||||||
|
canvas.height = WebcamHeight;
|
||||||
|
this.webcam.darkVideo.canvas = canvas;
|
||||||
|
this.webcam.darkVideo.ctx = ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.webcam.darkVideo.interval !== null) {
|
||||||
|
clearInterval(this.webcam.darkVideo.interval);
|
||||||
|
}
|
||||||
|
this.webcam.darkVideo.interval = setInterval(() => {
|
||||||
|
this.darkVideoInterval();
|
||||||
|
}, 5000);
|
||||||
|
},
|
||||||
|
stopDarkVideoDetection() {
|
||||||
|
if (this.webcam.darkVideo.interval !== null) {
|
||||||
|
clearInterval(this.webcam.darkVideo.interval);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
darkVideoInterval() {
|
||||||
|
if (!this.webcam.active) { // safety
|
||||||
|
this.stopDarkVideoDetection();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Take a screenshot from the user's local webcam.
|
||||||
|
let canvas = this.webcam.darkVideo.canvas,
|
||||||
|
ctx = this.webcam.darkVideo.ctx;
|
||||||
|
ctx.drawImage(this.webcam.elem, 0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
// Debugging: export the screenshot to a data URI.
|
||||||
|
let img = canvas.toDataURL('image/jpeg');
|
||||||
|
this.webcam.darkVideo.lastImage = img;
|
||||||
|
|
||||||
|
// Get average RGB value.
|
||||||
|
let rgb = this.getAverageRGB(ctx);
|
||||||
|
if (rgb === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.webcam.darkVideo.lastAverage = rgb;
|
||||||
|
this.webcam.darkVideo.lastAverageColor = `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 1)`;
|
||||||
|
|
||||||
|
// If the average total color is below the threshold (too dark of a video).
|
||||||
|
let averageBrightness = Math.floor((rgb[0] + rgb[1] + rgb[2]) / 3);
|
||||||
|
if (averageBrightness < this.webcam.darkVideo.threshold) {
|
||||||
|
if (this.wasTooDark) {
|
||||||
|
// Last sample was too dark too, = cut the camera.
|
||||||
|
this.stopVideo();
|
||||||
|
this.ChatClient(
|
||||||
|
"Your webcam was too dark to see anything and has been turned off.",
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Mark that this frame was too dark, if the next sample is too,
|
||||||
|
// cut their camera.
|
||||||
|
this.wasTooDark = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.wasTooDark = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getAverageRGB(ctx) {
|
||||||
|
// Helper function to compute the average color of a <canvas>.
|
||||||
|
// Ref: https://stackoverflow.com/a/2541680
|
||||||
|
const blockSize = 16; // only visit every N pixels
|
||||||
|
let img = null,
|
||||||
|
rgb = [0, 0, 0];
|
||||||
|
|
||||||
|
try {
|
||||||
|
img = ctx.getImageData(0, 0, WebcamWidth, WebcamHeight);
|
||||||
|
} catch(e) {
|
||||||
|
// Not supported.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let length = img.data.length,
|
||||||
|
i = 0,
|
||||||
|
count = 0,
|
||||||
|
firstColor = [],
|
||||||
|
allSame = true;
|
||||||
|
while ((i += blockSize * 4) < length) {
|
||||||
|
count++;
|
||||||
|
let thisColor = [
|
||||||
|
img.data[i],
|
||||||
|
img.data[i+1],
|
||||||
|
img.data[i+2]
|
||||||
|
]
|
||||||
|
|
||||||
|
rgb[0] += thisColor[0];
|
||||||
|
rgb[1] += thisColor[1];
|
||||||
|
rgb[2] += thisColor[2];
|
||||||
|
|
||||||
|
// Also check whether every sampled pixel is THE SAME color,
|
||||||
|
// to detect users broadcasting a solid (bright) color.
|
||||||
|
if (firstColor.length === 0) {
|
||||||
|
firstColor = [ rgb[0], rgb[1], rgb[2] ];
|
||||||
|
} else if (allSame) {
|
||||||
|
if (firstColor[0] !== thisColor[0] ||
|
||||||
|
firstColor[1] !== thisColor[1] ||
|
||||||
|
firstColor[2] !== thisColor[2]
|
||||||
|
) {
|
||||||
|
allSame = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If all sampled colors were the same solid image: red flag!
|
||||||
|
if (allSame) {
|
||||||
|
return [0, 0, 0];
|
||||||
|
}
|
||||||
|
|
||||||
|
rgb[0] = Math.floor(rgb[0]/count);
|
||||||
|
rgb[1] = Math.floor(rgb[1]/count);
|
||||||
|
rgb[2] = Math.floor(rgb[2]/count);
|
||||||
|
|
||||||
|
return rgb;
|
||||||
|
},
|
||||||
|
|
||||||
initHistory(channel) {
|
initHistory(channel) {
|
||||||
if (this.channels[channel] == undefined) {
|
if (this.channels[channel] == undefined) {
|
||||||
this.channels[channel] = {
|
this.channels[channel] = {
|
||||||
|
@ -2811,6 +2970,9 @@ export default {
|
||||||
} else if (this.imageDisplaySetting === "collapse") {
|
} else if (this.imageDisplaySetting === "collapse") {
|
||||||
// Put a collapser link.
|
// Put a collapser link.
|
||||||
let collapseID = `collapse-${messageID}`;
|
let collapseID = `collapse-${messageID}`;
|
||||||
|
if (!messageID) {
|
||||||
|
collapseID = "collapse-missingno-" + parseInt(Math.random()*100000);
|
||||||
|
}
|
||||||
message = `
|
message = `
|
||||||
<a href="#" id="img-show-${collapseID}"
|
<a href="#" id="img-show-${collapseID}"
|
||||||
class="button is-outlined is-small is-info"
|
class="button is-outlined is-small is-info"
|
||||||
|
|
Loading…
Reference in New Issue
Block a user