diff --git a/assets/js/modules/PlayerManager.mjs b/assets/js/modules/PlayerManager.mjs index 26cc8da..9a72dac 100644 --- a/assets/js/modules/PlayerManager.mjs +++ b/assets/js/modules/PlayerManager.mjs @@ -1,116 +1,35 @@ -import { default as PlayerWindow } from "./PlayerWindow.mjs"; -import { default as Monkeydo } from "./monkeydo/Monkeydo.mjs"; +import { default as Player } from "./PlayerWindow.mjs"; -export default class WindowManager { - constructor(mediaElement) { - const self = this; - this.mediaElement = mediaElement; - - // Bi-directional communcation to player windows - this.channels = { - "#lyrics": new BroadcastChannel("#lyrics"), - "#credits": new BroadcastChannel("#credits"), - "#art": new BroadcastChannel("#art") +export default class PlayerManager { + constructor() { + this.players = { + "lyrics": new Player("lyrics","monkeydo_lyrics.json")//, + //"credits": new Player("credits","monkeydo_credits.json"), + //"art": new Player("art") }; - for(const channel of Object.values(this.channels)) { + this.channels = new WeakMap(); + for(const player of Object.values(this.players)) { + // Create BroadcastChannels for each player + const channel = new BroadcastChannel(player.name); + this.channels.set(player,channel); channel.addEventListener("message",event => this.message(event)); - } - // Monkeydo methods - const methods = { - blank: (target) => { - self.channels[target].postMessage(["BLANK",target]); - }, - lineFeed: (target) => { - self.channels[target].postMessage(["LINE_FEED"]); - }, - textFeed: (text,target = "#lyrics") => { - self.channels[target].postMessage(["TEXT_FEED",text]); - }, - drawArt: (index,target = "#art") => { - self.channels[target].postMessage(["DRAW_ART",index]); - }, - playCredits: () => { - self.players.credits.play(); - } - } - - this.players = { - lyrics: new Monkeydo(methods), - credits: new Monkeydo(methods) + // Open each player + if(player.open() === null) return this.windowOpenFailed(); } } - playbackFailed(promiseObject = false) { - console.log(promiseObject); - } - - // Attempt to open a new window - async spawnPlayer(type) { - if(!type in this.channels) { - throw new Error(`Inavlid player type "${type}"`); - } - - return await new Promise((resolve,reject) => { - const player = new PlayerWindow(type).open(); - const channel = this.channels[type]; - - // Wait for window to emit ready state message before resolving - const ack = channel.addEventListener("message",event => { - if(event.data[0] === "WINDOW_READY" || event.data[1] === type) { - resolve("WINDOW_READY"); - } - // Window failed to initialize - if(event.data[0] === "WINDOW_ERROR" || event.data[1][0] === type) { - reject(event.data[1]); - } - return false; - }); - channel.removeEventListener("message",ack); - }); - } - - async play() { - for(const [key,player] of Object.entries(this.players)) { - const manifest = new URL(window.location.href + `monkeydo_${key}.json`); - - await player.load(manifest.toString()); - } - this.players.lyrics.play(); - this.mediaElement.play(); - } - - // Open player windows and start playback - async init() { - const art = this.spawnPlayer("#art"); - const credits = this.spawnPlayer("#credits"); - const lyrics = this.spawnPlayer("#lyrics"); - - const timeout = new Promise(resolve => setTimeout(() => resolve("TIMEOUT")),3000); - const windows = await Promise.allSettled([lyrics,credits,art]); - - // Wait for all windows to open and initialize (or timout and fail) - const status = Promise.race([windows,timeout]); - status.then(promises => { - promises.forEach(promiseObject => { - if(promiseObject.status !== "fulfilled") { - this.playbackFailed(promiseObject); - } - }); - - // Load Monkeydo manifest and start playback - this.play(); - }); + // Window blocked from opening, show "allow popups" message + windowOpenFailed() { + // Close all opened windows (some browsers allow one window to open) + //for(const player of Object.values(this.players)) { + // player.window?.close(); + //} + console.log("failed to open a window"); } message(event) { - const type = event.data[0]; - const data = event.data[1]; - - switch(type) { - case "PLAY": console.log("PLAY",event); break; - case "WINDOW_CLOSED": console.log("WINDOW_CLOSED",event); break; - } + console.log(event); } } \ No newline at end of file diff --git a/assets/js/modules/PlayerWindow.mjs b/assets/js/modules/PlayerWindow.mjs index 7216c7b..bb06b12 100644 --- a/assets/js/modules/PlayerWindow.mjs +++ b/assets/js/modules/PlayerWindow.mjs @@ -1,17 +1,17 @@ const windowPositions = { - "#lyrics": { + "lyrics": { width: window.innerWidth / 2, height: window.innerHeight, top: 0, left: 0 }, - "#credits": { + "credits": { width: window.innerWidth / 2, height: window.innerHeight / 2, top: 0, left: window.innerWidth / 2 }, - "#art": { + "art": { width: window.innerWidth / 2, height: window.innerHeight / 2, top: window.innerHeight / 2, @@ -20,7 +20,7 @@ const windowPositions = { } export default class PlayerWindow { - constructor(name) { + constructor(name,manifest = null) { this.features = { menubar: false, location: false, @@ -28,16 +28,20 @@ export default class PlayerWindow { scrollbar: false, status: false } + this.name = name; + this.window = null; this.url = new URL(window.location.href + "player"); this.url.hash = name; + if(manifest) this.url.searchParams.append("manifest",manifest); + // Copy window size rect into windowFeatures Object.assign(this.features,windowPositions[this.url.hash]); } // Convert windowFeatures object into a CSV DOMString - windowFeatures() { + getWindowFeatures() { let output = []; for(let [key,value] of Object.entries(this.features)) { if(typeof key === "boolean") { @@ -48,14 +52,12 @@ export default class PlayerWindow { return output.join(","); } + // Compile windowFeatures and open the window open() { - const features = this.windowFeatures(); - const open = window.open(this.url.toString(),this.url.hash,features); + const features = this.getWindowFeatures(); + this.window = window.open(this.url.toString(),this.name,features); - // Window failed to open (usually due to pop-up blocking), tell the WindowManager about this - if(!open) { - const channel = new BroadcastChannel(this.url.hash); - channel.postMessage(["WINDOW_ERROR",[this.url.hash,"BLOCKED"]]); - } + // Will return null if window failed to open (usually due to popup blocking) + return this.window; } } diff --git a/assets/js/modules/StillAlivePlayer.mjs b/assets/js/modules/StillAlivePlayer.mjs index 124fb55..12664a4 100644 --- a/assets/js/modules/StillAlivePlayer.mjs +++ b/assets/js/modules/StillAlivePlayer.mjs @@ -1,4 +1,6 @@ -// Encoded in order from: https://blog.kazitor.com/2014/12/portal-ascii/ +import { default as Monkeydo } from "./monkeydo/Monkeydo.mjs"; + +// URIEncoded ASCII art const artset = [ "%20%20%20%20%20%20%20%20%20%20%20%20%20.%2C-%3A%3B%2F%2F%3B%3A%3D%2C%0A%20%20%20%20%20%20%20%20%20.%20%3AH%40%40%40MM%40M%23H%2F.%2C%2B%25%3B%2C%0A%20%20%20%20%20%20%2C%2FX%2B%20%2BM%40%40M%40MM%25%3D%2C-%25HMMM%40X%2F%2C%0A%20%20%20%20%20-%2B%40MM%3B%20%24M%40%40MH%2B-%2C%3BXMMMM%40MMMM%40%2B-%0A%20%20%20%20%3B%40M%40%40M-%20XM%40X%3B.%20-%2BXXXXXHHH%40M%40M%23%40%2F.%0A%20%20%2C%25MM%40%40MH%20%2C%40%25%3D%20%20%20%20%20%20%20%20%20%20%20%20.---%3D-%3D%3A%3D%2C.%0A%20%20-%40%23%40%40%40MX%20.%2C%20%20%20%20%20%20%20%20%20%20%20%20%20%20-%25HX%24%24%25%25%25%2B%3B%0A%20%3D-.%2F%40M%40M%24%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20.%3B%40MMMM%40MM%3A%0A%20X%40%2F%20-%24MM%2F%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20.%2BMM%40%40%40M%24%0A%2C%40M%40H%3A%20%3A%40%3A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20.%20-X%23%40%40%40%40-%0A%2C%40%40%40MMX%2C%20.%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%2FH-%20%3B%40M%40M%3D%0A.H%40%40%40%40M%40%2B%2C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%25MM%2B..%25%23%24.%0A%20%2FMMMM%40MMH%2F.%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20XM%40MH%3B%20-%3B%0A%20%20%2F%25%2B%25%24XHH%40%24%3D%20%20%20%20%20%20%20%20%20%20%20%20%20%20%2C%20.H%40%40%40%40MX%2C%0A%20%20%20.%3D--------.%20%20%20%20%20%20%20%20%20%20%20-%25H.%2C%40%40%40%40%40MX%2C%0A%20%20%20.%25MM%40%40%40HHHXX%24%24%24%25%2B-%20.%3A%24MMX%20-M%40%40MM%25.%0A%20%20%20%20%20%3DXMMM%40MM%40MM%23H%3B%2C-%2BHMM%40M%2B%20%2FMMMX%3D%0A%20%20%20%20%20%20%20%3D%25%40M%40M%23%40%24-.%3D%24%40MM%40%40%40M%3B%20%25M%25%3D%0A%20%20%20%20%20%20%20%20%20%2C%3A%2B%24%2B-%2C%2FH%23MMMMMMM%40-%20-%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3D%2B%2B%25%25%25%25%2B%2F%3A-.", "%20%20%20%20%20%20%20%20%20%20%20%20%20%3D%2B%24HM%23%23%23%23%40H%25%3B%2C%0A%20%20%20%20%20%20%20%20%20%20%2FH%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23M%24%2C%0A%20%20%20%20%20%20%20%20%20%20%2C%40%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%2B%0A%20%20%20%20%20%20%20%20%20%20%20.H%23%23%23%23%23%23%23%23%23%23%23%23%23%23%2B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20X%23%23%23%23%23%23%23%23%23%23%23%23%2F%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%24%23%23%23%23%23%23%23%23%23%23%2F%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%25%23%23%23%23%23%23%23%23%2F%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%2FX%2F%3B%3B%2BX%2F%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20-XHHX-%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%2C%23%23%23%23%23%23%2C%0A%23%23%23%23%23%23%23%23%23%23%23%23%23X%20%20.M%23%23%23%23M.%20%20X%23%23%23%23%23%23%23%23%23%23%23%23%23%0A%23%23%23%23%23%23%23%23%23%23%23%23%23%23-%20%20%20-%2F%2F-%20%20%20-%23%23%23%23%23%23%23%23%23%23%23%23%23%23%0AX%23%23%23%23%23%23%23%23%23%23%23%23%23%23%25%2C%20%20%20%20%20%20%2C%2B%23%23%23%23%23%23%23%23%23%23%23%23%23%23X%0A-%23%23%23%23%23%23%23%23%23%23%23%23%23%23X%20%20%20%20%20%20%20%20X%23%23%23%23%23%23%23%23%23%23%23%23%23%23-%0A%20%25%23%23%23%23%23%23%23%23%23%23%23%23%25%20%20%20%20%20%20%20%20%20%20%25%23%23%23%23%23%23%23%23%23%23%23%23%25%0A%20%20%25%23%23%23%23%23%23%23%23%23%23%3B%20%20%20%20%20%20%20%20%20%20%20%20%3B%23%23%23%23%23%23%23%23%23%23%25%0A%20%20%20%3B%23%23%23%23%23%23%23M%3D%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3DM%23%23%23%23%23%23%23%3B%0A%20%20%20%20.%2BM%23%23%23%40%2C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%2C%40%23%23%23M%2B.%0A%20%20%20%20%20%20%20%3AXH.%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20.HX%3A", @@ -13,39 +15,70 @@ const artset = [ ]; export default class StillAlivePlayer { - constructor(element) { - this.channel = new BroadcastChannel(window.location.hash); + constructor(element,name) { + const self = this; + + this.name = name; + + // Open a BroadcastChannel to listen to messages for me + this.channel = new BroadcastChannel(this.name); this.channel.addEventListener("message",event => this.message(event)); - this.player = element; - - this.channel.postMessage(["WINDOW_READY",window.location.hash]); - } - - // Clear the screen from elements - blank() { - while(this.player.firstChild) { - this.player.removeChild(this.player.lastChild); + // Monkeydo methods + const methods = { + // Clear the screen from elements + blank: () => { + while(this.player.firstChild) { + this.player.removeChild(this.player.lastChild); + } + }, + // Create a new paragraph and make it the target for textFeed calls + lineFeed: () => { + this.target = document.createElement("p"); + this.player.appendChild(this.target); + }, + // Append text to the current target element + textFeed: (text) => { + this.target.innerText = this.target.innerText + text; + }, + // Decode and draw art from artset by key + drawArt: (index) => { + this.blank(); + self.target = document.createElement("pre"); + self.target.innerText = window.decodeURIComponent(artset[key]); + self.player.appendChild(self.target); + }, + playCredits: () => { + self.players.credits.play(); + } } + + // Execute or relay Monkeydo methods + const proxiedMethods = this.getMethods(methods); + + console.log(proxiedMethods.lineFeed("lyrics")); + this.player = element; } - // Create a new paragraph and make it the target for textFeed calls - lineFeed() { - this.target = document.createElement("p"); - this.player.appendChild(this.target); + getMethods(methods) { + const handler = { + get(target,propKey,receiver) { + const origMethod = target[propKey]; + return function (...args) { + console.log(this); + let result = origMethod.apply(this, args); + return result; + }; + } + }; + return new Proxy(methods,handler); } - // Append text to the current target element - textFeed(text) { - this.target.innerText = this.target.innerText + text; - } - - // Decode and draw art from artset by key - drawArt(key) { - this.blank(); - this.target = document.createElement("pre"); - this.target.innerText = window.decodeURIComponent(artset[key]); - this.player.appendChild(this.target); + // Open a channel to a different player to relay a task + relay(channelName,message) { + const channel = new BroadcastChannel(channelName); + channel.postMessage(message); + channel.close(); } message(event) { diff --git a/assets/js/player.mjs b/assets/js/player.mjs index 2462732..8ba980a 100644 --- a/assets/js/player.mjs +++ b/assets/js/player.mjs @@ -1,18 +1,14 @@ import { default as Player } from "./modules/StillAlivePlayer.mjs"; -// Go to start page if location.hash is omitted -if(!window.location.hash) { - // Close this window, if we can - window.close(); - - // Otherwise redirect to main page +if(typeof BroadcastChannel !== "function" || !window.location.hash) { + close(); const page = window.location.pathname.split("/"); const url = window.location.href.replace(page[page.length - 1],""); window.location.replace(url); -} +}; -// Add location.hash to body classList -document.body.classList.add(window.location.hash.substring(1)); +const name = window.location.hash.substring(1); +document.body.className = name; -const element = document.getElementById("player"); -const player = new Player(element); \ No newline at end of file +const target = document.getElementById("player"); +new Player(target,name); \ No newline at end of file diff --git a/assets/js/script.mjs b/assets/js/script.mjs index 2e112be..d5cda77 100755 --- a/assets/js/script.mjs +++ b/assets/js/script.mjs @@ -2,20 +2,11 @@ import { default as Player } from "./modules/PlayerManager.mjs"; const play = document.getElementById("play"); -try { - if(typeof BroadcastChannel !== "function") { - throw new Error("BroadcastChannel API is not supported"); - } - - const mediaElement = document.getElementById("still-alive"); - - const player = new Player(mediaElement); - play.addEventListener("click",() => player.init()); -} catch(error) { +if(typeof BroadcastChannel !== "function") { const message = document.getElementById("message"); - play.classList.add("unsupported"); play.innerText = "Your browser can not play this demo"; - message.innerText = error; -} \ No newline at end of file +} + +play.addEventListener("click",() => new Player()); \ No newline at end of file