Watermark QR code over webcam feeds to deter screen recording
This commit is contained in:
parent
a70d6d54b3
commit
f802de88ce
6
package-lock.json
generated
6
package-lock.json
generated
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
43
src/App.vue
43
src/App.vue
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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
27
src/lib/watermark.js
Normal 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;
|
Loading…
Reference in New Issue
Block a user