Pop-out draggable video support

This commit is contained in:
Noah 2023-04-19 22:00:31 -07:00
parent fb11295168
commit 58264515f9
4 changed files with 190 additions and 24 deletions

View File

@ -64,11 +64,11 @@ body {
grid-column: 2; grid-column: 2;
grid-row: 2; grid-row: 2;
overflow: hidden; overflow: hidden;
position: relative; /* position: relative; */
} }
/* Auto-scroll checkbox in the corner */ /* Auto-scroll checkbox in the corner */
.chat-column >.autoscroll-field { .autoscroll-field {
position: absolute; position: absolute;
z-index: 39; /* just below modal shadow */ z-index: 39; /* just below modal shadow */
right: 6px; right: 6px;
@ -175,7 +175,7 @@ body {
align-items: left; */ align-items: left; */
} }
.video-feeds > .feed { .feed {
position: relative; position: relative;
/* flex: 0 0 168px; */ /* flex: 0 0 168px; */
float: left; float: left;
@ -187,6 +187,17 @@ body {
resize: both; resize: both;
} }
/* A popped-out video feed window */
div.feed.popped-out {
position: absolute;
border: 1px solid #FFF;
cursor: move;
top: 0;
left: 0;
z-index: 1000;
resize: none;
}
.video-feeds.x1 > .feed { .video-feeds.x1 > .feed {
flex: 0 0 252px; flex: 0 0 252px;
width: 252px; width: 252px;
@ -211,25 +222,24 @@ body {
height: 448px; height: 448px;
} }
.video-feeds > .feed > video { .feed > video {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
.video-feeds > .feed > .controls { .feed > .controls {
position: absolute; position: absolute;
background: rgba(0, 0, 0, 0.75);
left: 4px; left: 4px;
bottom: 4px; bottom: 4px;
} }
.video-feeds > .feed > .close { .feed > .close {
position: absolute; position: absolute;
right: 4px; right: 4px;
top: 0; top: 0;
} }
.video-feeds > .feed > .caption { .feed > .caption {
position: absolute; position: absolute;
background: rgba(0, 0, 0, 0.75); background: rgba(0, 0, 0, 0.75);
color: #fff; color: #fff;

View File

@ -18,6 +18,9 @@ function setModalImage(url) {
return false; return false;
} }
// Popped-out video drag functions.
const app = Vue.createApp({ const app = Vue.createApp({
delimiters: ['[[', ']]'], delimiters: ['[[', ']]'],
@ -120,6 +123,7 @@ const app = Vue.createApp({
// Streams per username. // Streams per username.
streams: {}, streams: {},
muted: {}, // muted bool per username muted: {}, // muted bool per username
poppedOut: {}, // popped-out video per username
// RTCPeerConnections per username. // RTCPeerConnections per username.
pc: {}, pc: {},
@ -1183,6 +1187,111 @@ const app = Vue.createApp({
} }
}, },
// Pop out a user's video.
popoutVideo(username) {
this.WebRTC.poppedOut[username] = !this.WebRTC.poppedOut[username];
// If not popped out, reset CSS positioning.
window.requestAnimationFrame(this.makeDraggableVideos);
},
// Outside of Vue, attach draggable video scripts to DOM.
makeDraggableVideos() {
let $panel = document.querySelector("#video-feeds");
interact('.popped-in').unset();
// Give popped out videos to the root of the DOM so they can
// be dragged anywhere on the page.
window.requestAnimationFrame(() => {
document.querySelectorAll('.popped-out').forEach(node => {
// $panel.removeChild(node);
document.body.appendChild(node);
});
document.querySelectorAll('.popped-in').forEach(node => {
// document.body.removeChild(node);
$panel.appendChild(node);
node.style.top = null;
node.style.left = null;
node.setAttribute('data-x', 0);
node.setAttribute('data-y', 0);
});
});
interact('.popped-out').draggable({
// enable inertial throwing
inertia: true,
// keep the element within the area of it's parent
modifiers: [
interact.modifiers.restrictRect({
restriction: 'parent',
endOnly: true
})
],
listeners: {
// call this function on every dragmove event
move(event) {
let target = event.target;
let x = (parseFloat(target.getAttribute('data-x')) || 0) + event.dx
let y = (parseFloat(target.getAttribute('data-y')) || 0) + event.dy
target.style.top = `${y}px`;
target.style.left = `${x}px`;
target.setAttribute('data-x', x);
target.setAttribute('data-y', y);
},
// call this function on every dragend event
end (event) {
console.log(
'moved a distance of ' +
(Math.sqrt(Math.pow(event.pageX - event.x0, 2) +
Math.pow(event.pageY - event.y0, 2) | 0))
.toFixed(2) + 'px')
}
}
}).resizable({
edges: { left: true, right: true, bottom: true, right: true },
listeners: {
move (event) {
var target = event.target
var x = (parseFloat(target.getAttribute('data-x')) || 0)
var y = (parseFloat(target.getAttribute('data-y')) || 0)
// update the element's style
target.style.width = event.rect.width + 'px'
target.style.height = event.rect.height + 'px'
// translate when resizing from top or left edges
x += event.deltaRect.left
y += event.deltaRect.top
target.style.top = `${y}px`;
target.style.left = `${x}px`;
target.setAttribute('data-x', x)
target.setAttribute('data-y', y)
}
},
modifiers: [
// keep the edges inside the parent
interact.modifiers.restrictEdges({
outer: 'parent'
}),
// minimum size
interact.modifiers.restrictSize({
min: { width: 100, height: 50 }
})
],
inertia: true
})
},
initHistory(channel) { initHistory(channel) {
if (this.channels[channel] == undefined) { if (this.channels[channel] == undefined) {
this.channels[channel] = { this.channels[channel] = {

3
web/static/js/interact.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -451,7 +451,7 @@
<!-- Mute/Unmute my mic buttons (if streaming)--> <!-- Mute/Unmute my mic buttons (if streaming)-->
<button type="button" <button type="button"
v-if="webcam.active && !webcam.muted" v-if="webcam.active && !webcam.muted"
class="button is-small is-danger is-outlined ml-1 px-1" class="button is-small is-success ml-1 px-1"
@click="muteMe()"> @click="muteMe()">
<i class="fa fa-microphone mr-2"></i> <i class="fa fa-microphone mr-2"></i>
Mute Mute
@ -460,7 +460,7 @@
v-if="webcam.active && webcam.muted" v-if="webcam.active && webcam.muted"
class="button is-small is-danger ml-1 px-1" class="button is-small is-danger ml-1 px-1"
@click="muteMe()"> @click="muteMe()">
<i class="fa fa-microphone mr-2"></i> <i class="fa fa-microphone-slash mr-2"></i>
Unmute Unmute
</button> </button>
@ -561,14 +561,6 @@
<!-- Middle Column: Chat Room/History --> <!-- Middle Column: Chat Room/History -->
<div class="chat-column"> <div class="chat-column">
<div class="autoscroll-field tag">
<label class="checkbox is-size-6" title="Automatically scroll when new chat messages come in.">
<input type="checkbox"
v-model="autoscroll"
:value="true">
Auto-scroll
</label>
</div>
<div class="card grid-card"> <div class="card grid-card">
<header class="card-header has-background-link"> <header class="card-header has-background-link">
@ -615,19 +607,52 @@
</div> </div>
</div> </div>
</header> </header>
<div class="video-feeds" :class="webcam.videoScale" v-show="webcam.active || Object.keys(WebRTC.streams).length > 0"> <div id="video-feeds" class="video-feeds" :class="webcam.videoScale" v-show="webcam.active || Object.keys(WebRTC.streams).length > 0">
<!-- Video Feeds--> <!-- Video Feeds-->
<!-- My video --> <!-- My video -->
<div class="feed" v-show="webcam.active"> <div class="feed" v-show="webcam.active"
:class="{'popped-out': WebRTC.poppedOut[username],
'popped-in': !WebRTC.poppedOut[username]}">
<video class="feed" <video class="feed"
id="localVideo" id="localVideo"
autoplay muted> autoplay muted>
</video> </video>
<div class="caption">
[[username]]
</div>
<div class="controls">
<!-- MY Mute button -->
<button type="button"
v-if="webcam.active && !webcam.muted"
class="button is-small is-success is-outlined ml-1 px-2"
@click="muteMe()">
<i class="fa fa-microphone"></i>
</button>
<button type="button"
v-if="webcam.active && webcam.muted"
class="button is-small is-danger ml-1 px-2"
@click="muteMe()">
<i class="fa fa-microphone-slash"></i>
</button>
<!-- Pop-out MY video -->
<button type="button"
class="button is-small is-light is-outlined p-2 ml-2"
title="Pop out"
@click="popoutVideo(username)">
<i class="fa fa-up-right-from-square"></i>
</button>
</div>
</div> </div>
<!-- Others' videos --> <!-- Others' videos -->
<div class="feed" v-for="(stream, username) in WebRTC.streams" v-bind:key="username"> <div class="feed" v-for="(stream, username) in WebRTC.streams"
v-bind:key="username"
:class="{'popped-out': WebRTC.poppedOut[username],
'popped-in': !WebRTC.poppedOut[username]}">
<video class="feed" <video class="feed"
:id="'videofeed-'+username" :id="'videofeed-'+username"
autoplay> autoplay>
@ -644,20 +669,29 @@
</a> </a>
</div> </div>
<div class="controls"> <div class="controls">
<!-- Mute button -->
<button type="button" <button type="button"
v-if="isMuted(username)" v-if="isMuted(username)"
class="button is-small is-danger is-outlined p-1" class="button is-small is-danger p-2"
title="Unmute this video" title="Unmute this video"
@click="muteVideo(username)"> @click="muteVideo(username)">
<i class="fa fa-volume-xmark"></i> <i class="fa fa-volume-xmark"></i>
</button> </button>
<button type="button" <button type="button"
v-else v-else
class="button is-small is-danger is-outlined p-1" class="button is-small is-success is-outlined p-2"
title="Mute this video" title="Mute this video"
@click="muteVideo(username)"> @click="muteVideo(username)">
<i class="fa fa-volume-high"></i> <i class="fa fa-volume-high"></i>
</button> </button>
<!-- Pop-out -->
<button type="button"
class="button is-small is-light is-outlined p-2 ml-2"
title="Pop out"
@click="popoutVideo(username)">
<i class="fa fa-up-right-from-square"></i>
</button>
</div> </div>
</div> </div>
@ -680,6 +714,15 @@
</div> </div>
<div class="card-content" id="chatHistory" :class="fontSizeClass"> <div class="card-content" id="chatHistory" :class="fontSizeClass">
<div class="autoscroll-field tag">
<label class="checkbox is-size-6" title="Automatically scroll when new chat messages come in.">
<input type="checkbox"
v-model="autoscroll"
:value="true">
Auto-scroll
</label>
</div>
<!-- No history? --> <!-- No history? -->
<div v-if="chatHistory.length === 0"> <div v-if="chatHistory.length === 0">
<em v-if="isDM"> <em v-if="isDM">
@ -806,7 +849,7 @@
<div class="column pl-1 is-narrow"> <div class="column pl-1 is-narrow">
<button type="button" class="button" <button type="button" class="button"
@click="uploadFile()"> @click="uploadFile()">
<i class="fa fa-upload"></i> <i class="fa fa-image"></i>
</button> </button>
</div> </div>
</div> </div>
@ -979,6 +1022,7 @@ const UserJWTClaims = {{.JWTClaims.ToJSON}};
</script> </script>
<script src="/static/js/vue-3.2.45.js"></script> <script src="/static/js/vue-3.2.45.js"></script>
<script src="/static/js/interact.min.js"></script>
<script src="/static/js/sounds.js?{{.CacheHash}}"></script> <script src="/static/js/sounds.js?{{.CacheHash}}"></script>
<script src="/static/js/BareRTC.js?{{.CacheHash}}"></script> <script src="/static/js/BareRTC.js?{{.CacheHash}}"></script>