Add Proxy and window handler

This commit is contained in:
Victor Westerlund 2021-11-26 16:27:22 +01:00
parent a1981bc73a
commit 8b659066cc
5 changed files with 107 additions and 166 deletions

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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) {

View file

@ -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);
const target = document.getElementById("player");
new Player(target,name);

View file

@ -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;
}
}
play.addEventListener("click",() => new Player());