Video Syncronization Prototyping Complete.
This commit is contained in:
parent
0b68db1265
commit
6dc9ad7b34
|
|
@ -35,16 +35,19 @@ module.exports = class{
|
||||||
|
|
||||||
async handleConnection(userDB, chanDB, socket){
|
async handleConnection(userDB, chanDB, socket){
|
||||||
//send metadata to client
|
//send metadata to client
|
||||||
await this.sendClientMetadata();
|
this.sendClientMetadata();
|
||||||
|
|
||||||
//Send out emotes
|
//Send out emotes
|
||||||
await this.sendSiteEmotes();
|
this.sendSiteEmotes();
|
||||||
await this.sendChanEmotes(chanDB);
|
this.sendChanEmotes(chanDB);
|
||||||
await this.sendPersonalEmotes(userDB);
|
this.sendPersonalEmotes(userDB);
|
||||||
|
|
||||||
//Send out used tokes
|
//Send out used tokes
|
||||||
await this.sendUsedTokes(userDB);
|
this.sendUsedTokes(userDB);
|
||||||
|
|
||||||
|
//Send out the currently playing item
|
||||||
|
this.channel.queue.sendQueue(socket);
|
||||||
|
|
||||||
//Tattoo hashed IP address to user account for seven days
|
//Tattoo hashed IP address to user account for seven days
|
||||||
await userDB.tattooIPRecord(socket.handshake.address);
|
await userDB.tattooIPRecord(socket.handshake.address);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,22 +23,29 @@ module.exports = class{
|
||||||
this.server = server
|
this.server = server
|
||||||
//Set channel
|
//Set channel
|
||||||
this.channel = channel;
|
this.channel = channel;
|
||||||
//Create variable to hold sync delta
|
//Create variable to hold sync delta in ms
|
||||||
this.syncDelta = 1000;
|
this.syncDelta = 1000;
|
||||||
|
//Create variable to hold current timestamp within the video
|
||||||
|
this.timestamp = 0;
|
||||||
//Create variable to hold sync timer
|
//Create variable to hold sync timer
|
||||||
this.syncTimer = null;
|
this.syncTimer = null;
|
||||||
//Create variable to hold currently playing media object
|
//Create variable to hold currently playing media object
|
||||||
this.nowPlaying = null;
|
this.nowPlaying = null;
|
||||||
//Create variable to hold current timestamp within the video
|
|
||||||
this.timestamp = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
queueMedia(inputMedia){
|
queueMedia(inputMedia){
|
||||||
//Create new media object, start it now
|
//Create new media object, set start time to now
|
||||||
const mediaObj = queuedMedia.fromMedia(inputMedia, new Date().getTime());
|
const mediaObj = queuedMedia.fromMedia(inputMedia, new Date().getTime());
|
||||||
|
|
||||||
|
//Start playback
|
||||||
|
this.play(mediaObj);
|
||||||
}
|
}
|
||||||
|
|
||||||
play(mediaObj){
|
play(mediaObj){
|
||||||
|
//Silently end the media
|
||||||
|
this.end(true);
|
||||||
|
|
||||||
//reset current timestamp
|
//reset current timestamp
|
||||||
this.timestamp = 0;
|
this.timestamp = 0;
|
||||||
|
|
||||||
|
|
@ -46,7 +53,7 @@ module.exports = class{
|
||||||
this.nowPlaying = mediaObj;
|
this.nowPlaying = mediaObj;
|
||||||
|
|
||||||
//Send play signal out to the channel
|
//Send play signal out to the channel
|
||||||
this.server.io.in(this.channel.name).emit("play", {media: this.nowPlaying});
|
this.sendQueue();
|
||||||
|
|
||||||
//Kick off the sync timer
|
//Kick off the sync timer
|
||||||
this.syncTimer = setTimeout(this.sync.bind(this), this.syncDelta);
|
this.syncTimer = setTimeout(this.sync.bind(this), this.syncDelta);
|
||||||
|
|
@ -56,14 +63,58 @@ module.exports = class{
|
||||||
//Send sync signal out to the channel
|
//Send sync signal out to the channel
|
||||||
this.server.io.in(this.channel.name).emit("sync", {timestamp: this.timestamp});
|
this.server.io.in(this.channel.name).emit("sync", {timestamp: this.timestamp});
|
||||||
|
|
||||||
//If the media hasn't finished playing
|
//If the media has over a second to go
|
||||||
if(this.timeStamp < this.nowPlaying.duration){
|
if((this.timestamp + 1) < this.nowPlaying.duration){
|
||||||
|
|
||||||
//Increment the time stamp
|
//Increment the time stamp
|
||||||
this.timestamp++;
|
this.timestamp += (this.syncDelta / 1000);
|
||||||
|
|
||||||
//Call the sync function in another second
|
//Call the sync function in another second
|
||||||
this.syncTimer = setTimeout(this.sync.bind(this), this.syncDelta);
|
this.syncTimer = setTimeout(this.sync.bind(this), this.syncDelta);
|
||||||
|
}else{
|
||||||
|
//Get leftover video length in ms
|
||||||
|
const leftover = (this.nowPlaying.duration - this.timestamp) * 1000;
|
||||||
|
|
||||||
|
//Call the end function once the video is over
|
||||||
|
this.syncTimer = setTimeout(this.end.bind(this), leftover);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
end(quiet = false){
|
||||||
|
//Call off any existing sync timer
|
||||||
|
clearTimeout(this.syncTimer);
|
||||||
|
|
||||||
|
//Clear out the sync timer
|
||||||
|
this.syncTimer = null;
|
||||||
|
|
||||||
|
//Clear now playing
|
||||||
|
this.nowPlaying = null;
|
||||||
|
|
||||||
|
//Clear timestamp
|
||||||
|
this.timestamp = 0;
|
||||||
|
|
||||||
|
//If we're not being quiet
|
||||||
|
if(!quiet){
|
||||||
|
//Tell everyone of the end-times
|
||||||
|
this.server.io.in(this.channel.name).emit('end', {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sendQueue(socket){
|
||||||
|
//Create data object
|
||||||
|
const data = {
|
||||||
|
media: this.nowPlaying,
|
||||||
|
timestamp: this.timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
//If a socket is specified
|
||||||
|
if(socket != null){
|
||||||
|
//Send data out to specified socket
|
||||||
|
socket.emit("play", data);
|
||||||
|
//Otherwise
|
||||||
|
}else{
|
||||||
|
//Send that shit out to the entire channel
|
||||||
|
this.server.io.in(this.channel.name).emit("play", data);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -72,7 +72,7 @@ module.exports = class{
|
||||||
const link = `https://archive.org/download/${mediaInfo.metadata.identifier}/${file.name}`
|
const link = `https://archive.org/download/${mediaInfo.metadata.identifier}/${file.name}`
|
||||||
|
|
||||||
//Create new media object from file info
|
//Create new media object from file info
|
||||||
mediaList.push(new media(name, name, link, 'ia', file.length));
|
mediaList.push(new media(name, name, link, 'ia', Number(file.length)));
|
||||||
}
|
}
|
||||||
|
|
||||||
//return media object list
|
//return media object list
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ module.exports.fetchMetadata = async function(link){
|
||||||
|
|
||||||
function compatibilityFilter(file){
|
function compatibilityFilter(file){
|
||||||
//return true for all files that match for web-safe formats
|
//return true for all files that match for web-safe formats
|
||||||
return file.format == "h.264"
|
return file.format == "h.264" || file.format == "Ogg Video" || file.format.match("MPEG4");
|
||||||
}
|
}
|
||||||
|
|
||||||
function pathFilter(file){
|
function pathFilter(file){
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. %>
|
||||||
<script src="/js/channel/chatPostprocessor.js"></script>
|
<script src="/js/channel/chatPostprocessor.js"></script>
|
||||||
<script src="/js/channel/chat.js"></script>
|
<script src="/js/channel/chat.js"></script>
|
||||||
<script src="/js/channel/userlist.js"></script>
|
<script src="/js/channel/userlist.js"></script>
|
||||||
|
<script src="/js/channel/mediaHandler.js"></script>
|
||||||
<script src="/js/channel/player.js"></script>
|
<script src="/js/channel/player.js"></script>
|
||||||
<script src="/js/channel/cpanel.js"></script>
|
<script src="/js/channel/cpanel.js"></script>
|
||||||
<script src="/js/channel/panels/emotePanel.js"></script>
|
<script src="/js/channel/panels/emotePanel.js"></script>
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. %>
|
||||||
<div class="media-panel" id="media-panel-div">
|
<div class="media-panel" id="media-panel-div">
|
||||||
<div class="media-panel panel-head-div" id="media-panel-head-div">
|
<div class="media-panel panel-head-div" id="media-panel-head-div">
|
||||||
<i class="media-panel panel-head-element bi-caret-down-fill" id="media-panel-div-toggle-icon"></i>
|
<i class="media-panel panel-head-element bi-caret-down-fill" id="media-panel-div-toggle-icon"></i>
|
||||||
<p class="media-panel panel-head-element" id="media-panel-title-paragraph">Currently Playing: NULL</p>
|
<p class="media-panel panel-head-element" id="media-panel-title-paragraph">Currently Playing: <span id="media-panel-title-span">NULL</span></p>
|
||||||
<span class="media-panel panel-head-spacer-span" id="media-panel-head-spacer-span"></span>
|
<span class="media-panel panel-head-spacer-span" id="media-panel-head-spacer-span"></span>
|
||||||
<i class="media-panel panel-head-element bi-arrow-repeat" id="media-panel-sync-icon"></i>
|
<i class="media-panel panel-head-element bi-arrow-repeat" id="media-panel-sync-icon"></i>
|
||||||
<i class="media-panel panel-head-element bi-aspect-ratio-fill" id="media-panel-aspect-lock-icon"></i>
|
<i class="media-panel panel-head-element bi-aspect-ratio-fill" id="media-panel-aspect-lock-icon"></i>
|
||||||
|
|
@ -27,5 +27,6 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. %>
|
||||||
<i class="media-panel panel-head-element bi-arrow-clockwise" id="media-panel-reload-icon"></i>
|
<i class="media-panel panel-head-element bi-arrow-clockwise" id="media-panel-reload-icon"></i>
|
||||||
<i class="media-panel panel-head-element bi-chat-right-dots-fill" id="media-panel-show-chat-icon"></i>
|
<i class="media-panel panel-head-element bi-chat-right-dots-fill" id="media-panel-show-chat-icon"></i>
|
||||||
</div>
|
</div>
|
||||||
<video src="/video/static.webm" class="media-panel" id="media-panel-video" muted loop autoplay></video>
|
<div id="media-panel-video-container">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -13,6 +13,8 @@ GNU Affero General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
You should have received a copy of the GNU Affero General Public License
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>. %>
|
along with this program. If not, see <https://www.gnu.org/licenses/>. %>
|
||||||
|
<%# Technically favicon has nothing to do with .css, but it's still looks related, uses a link tag, and globally used :P %>
|
||||||
|
<link rel="icon" type="image/x-icon" href="/img/sweet_leaf_simple.png">
|
||||||
<link rel="stylesheet" href="/lib/bootstrap-icons/font/bootstrap-icons.css">
|
<link rel="stylesheet" href="/lib/bootstrap-icons/font/bootstrap-icons.css">
|
||||||
<link rel="stylesheet" type="text/css" href="/css/global.css">
|
<link rel="stylesheet" type="text/css" href="/css/global.css">
|
||||||
<link rel="stylesheet" type="text/css" href="/css/flair.css">
|
<link rel="stylesheet" type="text/css" href="/css/flair.css">
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,13 @@ div#chat-panel-main-div{
|
||||||
height: 1%;
|
height: 1%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#media-panel-video-container{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.drag-handle{
|
.drag-handle{
|
||||||
position: absolute;
|
position: absolute;
|
||||||
cursor: ew-resize;
|
cursor: ew-resize;
|
||||||
|
|
|
||||||
146
www/js/channel/mediaHandler.js
Normal file
146
www/js/channel/mediaHandler.js
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
/*Canopy - The next generation of stoner streaming software
|
||||||
|
Copyright (C) 2024-2025 Rainbownapkin and the TTN Community
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.*/
|
||||||
|
|
||||||
|
class mediaHandler{
|
||||||
|
constructor(client, player, media){
|
||||||
|
//Get parents
|
||||||
|
this.client = client;
|
||||||
|
this.player = player;
|
||||||
|
this.syncTolerance = 1;
|
||||||
|
this.syncDelta = 6;
|
||||||
|
|
||||||
|
//Ingest media object from server
|
||||||
|
this.startMedia(media);
|
||||||
|
}
|
||||||
|
|
||||||
|
startMedia(media){
|
||||||
|
//If we properly ingested the media
|
||||||
|
if(this.ingestMedia(media)){
|
||||||
|
//Build the video player
|
||||||
|
this.buildPlayer();
|
||||||
|
|
||||||
|
//Call the start function
|
||||||
|
this.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildPlayer(){
|
||||||
|
//Create player
|
||||||
|
this.video = document.createElement('video');
|
||||||
|
//Append it to page
|
||||||
|
this.player.videoContainer.appendChild(this.video);
|
||||||
|
//Reset player lock
|
||||||
|
this.lock = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
destroyPlayer(){
|
||||||
|
//Remove player from page
|
||||||
|
this.video.remove();
|
||||||
|
//Null out video property
|
||||||
|
this.video = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
ingestMedia(media){
|
||||||
|
//Set now playing
|
||||||
|
this.nowPlaying = media;
|
||||||
|
|
||||||
|
//return true to signify success
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
start(){
|
||||||
|
}
|
||||||
|
|
||||||
|
sync(timestamp){
|
||||||
|
}
|
||||||
|
|
||||||
|
end(){
|
||||||
|
this.nowPlaying = null;
|
||||||
|
this.destroyPlayer();
|
||||||
|
}
|
||||||
|
|
||||||
|
setPlayerLock(lock){
|
||||||
|
//toggle controls
|
||||||
|
this.video.controls = !lock;
|
||||||
|
//Only toggle mute if we're locking, or if we're unlocking after being locked
|
||||||
|
//If this is ran twice without locking we don't want to surprise unmute on the user
|
||||||
|
if(lock || this.lock){
|
||||||
|
//toggle mute
|
||||||
|
this.video.muted = lock;
|
||||||
|
}
|
||||||
|
//toggle looping
|
||||||
|
this.video.loop = lock;
|
||||||
|
//set lock property
|
||||||
|
this.lock = lock;
|
||||||
|
}
|
||||||
|
|
||||||
|
getRatio(){
|
||||||
|
return this.video.videoWidth / this.video.videoHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class nullHandler extends mediaHandler{
|
||||||
|
constructor(client, player){
|
||||||
|
//Call derived constructor
|
||||||
|
super(client, player, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
start(){
|
||||||
|
//Lock the player
|
||||||
|
super.setPlayerLock(true);
|
||||||
|
|
||||||
|
//Set the static placeholder
|
||||||
|
this.video.src = '/video/static.webm';
|
||||||
|
|
||||||
|
//Set video title
|
||||||
|
this.player.title.textContent = 'NULL';
|
||||||
|
|
||||||
|
//play the placeholder video
|
||||||
|
this.video.play();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class rawFileHandler extends mediaHandler{
|
||||||
|
constructor(client, player, media){
|
||||||
|
//Call derived constructor
|
||||||
|
super(client, player, media);
|
||||||
|
}
|
||||||
|
|
||||||
|
start(){
|
||||||
|
//Set video
|
||||||
|
this.video.src = this.nowPlaying.id;
|
||||||
|
|
||||||
|
//Set video title
|
||||||
|
this.player.title.textContent = this.nowPlaying.title;
|
||||||
|
|
||||||
|
//Unlock player
|
||||||
|
super.setPlayerLock(false);
|
||||||
|
|
||||||
|
//play video
|
||||||
|
this.video.play();
|
||||||
|
}
|
||||||
|
|
||||||
|
sync(timestamp){
|
||||||
|
//Check if timestamp evenly devides into sync delta, effectively only checking for sync every X seconds
|
||||||
|
if(timestamp % this.syncDelta == 0){
|
||||||
|
//Get absolute difference between syncronization timestamp and actual video timestamp, and check if it's over the sync tolerance
|
||||||
|
if(Math.abs(timestamp - this.video.currentTime) > this.syncTolerance){
|
||||||
|
//If we need to sync, then sync the video!
|
||||||
|
this.video.currentTime = timestamp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -27,15 +27,17 @@ class player{
|
||||||
|
|
||||||
//elements
|
//elements
|
||||||
this.playerDiv = document.querySelector("#media-panel-div");
|
this.playerDiv = document.querySelector("#media-panel-div");
|
||||||
|
this.videoContainer = document.querySelector("#media-panel-video-container")
|
||||||
this.navBar = document.querySelector("#navbar");
|
this.navBar = document.querySelector("#navbar");
|
||||||
this.video = document.querySelector("#media-panel-video");
|
|
||||||
this.uiBar = document.querySelector("#media-panel-head-div");
|
this.uiBar = document.querySelector("#media-panel-head-div");
|
||||||
|
this.title = document.querySelector("#media-panel-title-span");
|
||||||
this.showVideoIcon = document.querySelector("#chat-panel-show-video-icon");
|
this.showVideoIcon = document.querySelector("#chat-panel-show-video-icon");
|
||||||
this.hideVideoIcon = document.querySelector("#media-panel-div-toggle-icon");
|
this.hideVideoIcon = document.querySelector("#media-panel-div-toggle-icon");
|
||||||
this.cinemaModeIcon = document.querySelector("#media-panel-cinema-mode-icon");
|
this.cinemaModeIcon = document.querySelector("#media-panel-cinema-mode-icon");
|
||||||
|
|
||||||
//run setup functions
|
//run setup functions
|
||||||
this.setupInput();
|
this.setupInput();
|
||||||
|
this.defineListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
setupInput(){
|
setupInput(){
|
||||||
|
|
@ -50,6 +52,52 @@ class player{
|
||||||
this.cinemaModeIcon.addEventListener("click", ()=>{this.toggleCinemaMode()});
|
this.cinemaModeIcon.addEventListener("click", ()=>{this.toggleCinemaMode()});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defineListeners(){
|
||||||
|
this.client.socket.on("play", this.play.bind(this));
|
||||||
|
this.client.socket.on("sync", this.sync.bind(this));
|
||||||
|
this.client.socket.on("end", this.end.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
play(data){
|
||||||
|
//If we have an active media handler
|
||||||
|
if(this.mediaHandler != null){
|
||||||
|
//End the media handler
|
||||||
|
this.mediaHandler.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
//Ignore null media
|
||||||
|
if(data.media == null){
|
||||||
|
//Set null handler
|
||||||
|
this.mediaHandler = new nullHandler(client, this);
|
||||||
|
//Otherwise
|
||||||
|
}else{
|
||||||
|
//If we have a raw-file compatible source
|
||||||
|
if(data.media.type == 'ia' || data.media.type == 'raw'){
|
||||||
|
//Create a new raw file handler for it
|
||||||
|
this.mediaHandler = new rawFileHandler(client, this, data.media);
|
||||||
|
//Sync to time stamp
|
||||||
|
this.mediaHandler.sync(data.timestamp);
|
||||||
|
}else{
|
||||||
|
this.mediaHandler = new nullHandler(client, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Re-size to aspect since video may now be a different size
|
||||||
|
this.client.chatBox.resizeAspect();
|
||||||
|
}
|
||||||
|
|
||||||
|
end(){
|
||||||
|
//Call the media handler finisher
|
||||||
|
this.mediaHandler.end();
|
||||||
|
|
||||||
|
//Replace it with a null handler
|
||||||
|
this.mediaHandler = new nullHandler(client, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
sync(data){
|
||||||
|
this.mediaHandler.sync(data.timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
popUI(event){
|
popUI(event){
|
||||||
this.toggleUI(true);
|
this.toggleUI(true);
|
||||||
clearTimeout(this.uiTimer);
|
clearTimeout(this.uiTimer);
|
||||||
|
|
@ -94,7 +142,14 @@ class player{
|
||||||
this.popUI();
|
this.popUI();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//This way other classes don't need to worry about media handler
|
||||||
getRatio(){
|
getRatio(){
|
||||||
return this.video.videoWidth / this.video.videoHeight;
|
//If we have no media handler
|
||||||
|
if(this.mediaHandler == null){
|
||||||
|
//Return a 4/3 aspect to get a decent chat size
|
||||||
|
return 4/3;
|
||||||
|
}else{
|
||||||
|
return this.mediaHandler.getRatio();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in a new issue