Watermark QR code over webcam feeds to deter screen recording

This commit is contained in:
Noah 2024-10-02 20:33:57 -07:00
parent a70d6d54b3
commit f802de88ce
5 changed files with 101 additions and 10 deletions

6
package-lock.json generated
View File

@ -10,6 +10,7 @@
"dependencies": { "dependencies": {
"floating-vue": "^2.0.0-beta.24", "floating-vue": "^2.0.0-beta.24",
"interactjs": "^1.10.18", "interactjs": "^1.10.18",
"qrcodejs": "github:danielgjackson/qrcodejs",
"vue": "^3.3.4", "vue": "^3.3.4",
"vue-mention": "^2.0.0-alpha.3", "vue-mention": "^2.0.0-alpha.3",
"vue3-emoji-picker": "^1.1.7", "vue3-emoji-picker": "^1.1.7",
@ -1680,6 +1681,11 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/qrcodejs": {
"version": "0.0.0",
"resolved": "git+ssh://git@github.com/danielgjackson/qrcodejs.git#86770ec12f0f9abee8728fc9018ab7bd0949f4bc",
"license": "BSD-2-Clause"
},
"node_modules/queue-microtask": { "node_modules/queue-microtask": {
"version": "1.2.3", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",

View File

@ -12,6 +12,7 @@
"dependencies": { "dependencies": {
"floating-vue": "^2.0.0-beta.24", "floating-vue": "^2.0.0-beta.24",
"interactjs": "^1.10.18", "interactjs": "^1.10.18",
"qrcodejs": "github:danielgjackson/qrcodejs",
"vue": "^3.3.4", "vue": "^3.3.4",
"vue-mention": "^2.0.0-alpha.3", "vue-mention": "^2.0.0-alpha.3",
"vue3-emoji-picker": "^1.1.7", "vue3-emoji-picker": "^1.1.7",

View File

@ -19,6 +19,7 @@ import LocalStorage from './lib/LocalStorage';
import VideoFlag from './lib/VideoFlag'; import VideoFlag from './lib/VideoFlag';
import StatusMessage from './lib/StatusMessage'; import StatusMessage from './lib/StatusMessage';
import { SoundEffects, DefaultSounds } from './lib/sounds'; import { SoundEffects, DefaultSounds } from './lib/sounds';
import WatermarkImage from './lib/watermark';
// WebRTC configuration. // WebRTC configuration.
const configuration = { const configuration = {
@ -181,6 +182,10 @@ export default {
rememberExpresslyClosed: true, // remember cams we expressly closed rememberExpresslyClosed: true, // remember cams we expressly closed
autoMuteWebcams: false, // auto-mute other cameras' audio channels autoMuteWebcams: false, // auto-mute other cameras' audio channels
// My watermark image for screen recording protection.
// Set after login in setWatermark.
watermark: null,
// Who all is watching me? map of users. // Who all is watching me? map of users.
watching: {}, watching: {},
@ -1630,6 +1635,10 @@ export default {
onLoggedIn() { onLoggedIn() {
// Called after the first 'me' is received from the chat server, e.g. once per login. // Called after the first 'me' is received from the chat server, e.g. once per login.
// Load our watermark image.
this.webcam.watermark = WatermarkImage(this.username);
this.ChatClient(`Watermark image created: <img src="${this.webcam.watermark}" width="120">`);
// Do we auto-broadcast our camera? // Do we auto-broadcast our camera?
if (this.webcam.autoshare) { if (this.webcam.autoshare) {
this.startVideo({ force: true }); this.startVideo({ force: true });
@ -4709,20 +4718,36 @@ export default {
<!-- Video Feeds--> <!-- Video Feeds-->
<!-- My video --> <!-- My video -->
<VideoFeed v-show="webcam.active" :local-video="true" :username="username" <VideoFeed v-show="webcam.active"
:popped-out="WebRTC.poppedOut[username]" :is-explicit="webcam.nsfw" :is-muted="webcam.muted" :local-video="true"
:is-source-muted="webcam.muted" @mute-video="muteMe()" @popout="popoutVideo" :username="username"
:popped-out="WebRTC.poppedOut[username]"
:is-explicit="webcam.nsfw"
:is-muted="webcam.muted"
:is-source-muted="webcam.muted"
:watermark-image="webcam.watermark"
@mute-video="muteMe()"
@popout="popoutVideo"
@open-profile="showProfileModal" @open-profile="showProfileModal"
@set-volume="setVideoVolume"> @set-volume="setVideoVolume">
</VideoFeed> </VideoFeed>
<!-- Others' videos --> <!-- Others' videos -->
<VideoFeed v-for="(stream, username) in WebRTC.streams" v-bind:key="username" :username="username" <VideoFeed v-for="(stream, username) in WebRTC.streams"
:popped-out="WebRTC.poppedOut[username]" :is-explicit="isUsernameCamNSFW(username)" v-bind:key="username"
:is-source-muted="isSourceMuted(username)" :is-muted="isMuted(username)" :username="username"
:is-watching-me="isWatchingMe(username)" :is-frozen="WebRTC.frozenStreamDetected[username]" :popped-out="WebRTC.poppedOut[username]"
@reopen-video="openVideoByUsername" @mute-video="muteVideo" @popout="popoutVideo" :is-explicit="isUsernameCamNSFW(username)"
@close-video="expresslyCloseVideo" @set-volume="setVideoVolume" :is-source-muted="isSourceMuted(username)"
:is-muted="isMuted(username)"
:is-watching-me="isWatchingMe(username)"
:is-frozen="WebRTC.frozenStreamDetected[username]"
:watermark-image="webcam.watermark"
@reopen-video="openVideoByUsername"
@mute-video="muteVideo"
@popout="popoutVideo"
@close-video="expresslyCloseVideo"
@set-volume="setVideoVolume"
@open-profile="showProfileModal"> @open-profile="showProfileModal">
</VideoFeed> </VideoFeed>

View File

@ -11,6 +11,7 @@ export default {
isSourceMuted: Boolean, // camera is muted on the broadcaster's end isSourceMuted: Boolean, // camera is muted on the broadcaster's end
isWatchingMe: Boolean, // other video is watching us back isWatchingMe: Boolean, // other video is watching us back
isFrozen: Boolean, // video is detected as frozen isFrozen: Boolean, // video is detected as frozen
watermarkImage: Image, // watermark image to overlay (nullable)
}, },
components: { components: {
Slider, Slider,
@ -102,7 +103,13 @@ export default {
'popped-out': poppedOut, 'popped-out': poppedOut,
'popped-in': !poppedOut, 'popped-in': !poppedOut,
}" @mouseover="mouseOver = true" @mouseleave="mouseOver = false"> }" @mouseover="mouseOver = true" @mouseleave="mouseOver = false">
<video class="feed" :id="videoID" autoplay :muted="localVideo" playsinline></video> <video class="feed" :id="videoID" autoplay disablepictureinpicture :muted="localVideo" playsinline></video>
<!-- Watermark layer -->
<div v-if="watermarkImage">
<img :src="watermarkImage" class="watermark">
<img :src="watermarkImage" class="corner-watermark seethru invert-color">
</div>
<!-- Caption --> <!-- Caption -->
<div class="caption" :class="textColorClass"> <div class="caption" :class="textColorClass">
@ -187,4 +194,29 @@ video {
.seethru { .seethru {
opacity: 0.4; opacity: 0.4;
} }
/* Watermark image */
.watermark {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
width: 40%;
height: 40%;
opacity: 0.02;
}
.corner-watermark {
position: absolute;
right: 4px;
bottom: 4px;
width: 20%;
min-width: 32px;
min-height: 32px;
max-height: 20%;
}
.invert-color {
filter: invert(100%);
}
</style> </style>

27
src/lib/watermark.js Normal file
View File

@ -0,0 +1,27 @@
import QrCode from 'qrcodejs';
// WatermarkImage outputs a QR code containing watermark data about the current user.
//
// To help detect when someone has screen recorded and shared it, and being able to know who/when/etc.
function WatermarkImage(username) {
let now = new Date();
let dateString = [
now.getFullYear(),
('0' + (now.getMonth()+1)).slice(-2),
('0' + (now.getDate())).slice(-2),
].join('-');
let fields = [
window.location.hostname,
username,
dateString,
].join(' ');
console.error("watermark message:", fields);
const matrix = QrCode.generate(fields);
const uri = QrCode.render('svg-uri', matrix);
return uri;
}
export default WatermarkImage;