diff --git a/teletext/tv.js b/teletext/tv.js new file mode 100644 index 0000000..cb637ed --- /dev/null +++ b/teletext/tv.js @@ -0,0 +1,1947 @@ +"use strict"; + +let channelMedia; +let videoVideo; +let videoAudio; +let videoInterval; + +function initTV(){ + powerOnFlag = false; + renderer.enable(false); + teletextOnFlag = false; + channelNumber = getLocalStorage("channelNumber"); + channelNumber = (channelNumber == null)?1:parseInt(channelNumber); /* default to 1 */ + renderLevel = 2; + + document.addEventListener("visibilitychange", function() { + if (document.hidden){ + hiddenTimeout = setTimeout(websocketDisconnect, 60000); // allow to go to background for 60 seconds before dropping connection + } else { + clearTimeout(hiddenTimeout); + if (powerOnFlag){ + if (websocket && websocket.readyState > 1){ + websocketConnect(); + } + } + } + }); + + channelMedia = false; + + videoImage = new Image(); + videoImage.setAttribute(channelMedia, null); + videoImage.addEventListener("load", imageLoaded); + + videoVideo = document.createElement('video'); + videoVideo.setAttribute(channelMedia, null); + videoVideo.addEventListener("loadeddata", videoLoaded); + + videoAudio = document.createElement('audio'); + videoAudio.setAttribute(channelMedia, null); + videoAudio.addEventListener("loadeddata", audioLoaded); + + let vol = getLocalStorage("videoVolume"); + if (vol === null) + vol = 0.5; + videoVideo.volume = vol; + videoAudio.volume = vol; + videoVideo.muted = getLocalStorage("videoMuted"); + videoAudio.muted = getLocalStorage("videoMuted"); + + videoAudio.addEventListener("ended", audioLoaded); + videoVideo.loop = true; + videoVideo.addEventListener("play", function(){ + staticEffect(false); + clearInterval(videoInterval); + videoInterval = setInterval(blitVideo,40); // update at 25 fps + }); + + if (hashStringProperties.id){ + noChannel = true; // inhibit channel number on next channel change + channelID = hashStringProperties.id + powerbuttonhandler(); + } + + window.onbeforeunload = function(){if (powerOnFlag) powerbuttonhandler();}; +} + +let poweroffTimeout; +let reconnectCount = 0; + +function resetPowerOffTimeout() +{ + clearTimeout(poweroffTimeout); + reconnectCount = 0; /* reset bouncing connection trap */ + if (autoPowerOffTime > 0) + poweroffTimeout = setTimeout(function(){if (powerOnFlag){powerbuttonhandler();}}, autoPowerOffTime * 60000); +} + +let websocketFailedFlag; +let nextWebSocketFailedFlag; +let connectingTimeout; + +function websocketConnect(){ + clearTimeout(reconnectTimeout); + let id + try { + id = JSON.parse(atob(channelID)) + } catch { + console.log("bad channel ID "+channelID); + return; + } + + try { + websocket = new WebSocket("wss://"+id[1]+id[2]); + } catch (e){ + console.log("failed to create websocket connection ",e); + } + connectingTimeout = setTimeout(function(){websocketFailedFlag = true; failPage();}, 2000); // workaround for firefox slowing repeated reconnect attempts + websocket.binaryType = 'arraybuffer'; + websocket.onopen = function() { + clearTimeout(connectingTimeout); + reconnectCount++; + + settingReceived = false; // clear flag to detect new server response + websocket.send(["service", channelID]); + + websocket.send(["ttx", teletextOnFlag]); + + if (teletextOnFlag){ + websocket.send(["pagesearch", 0, slots[0].m, slots[0].p, slots[0].s, pagesearch, pagehold, false]); + + for (let i=1; i 0x8FF) || ((hashpage & 0xFF) == 0xFF) || isNaN(hashpage)){ + delete hashStringProperties.page; + } else { + initialmagazine = (hashpage&0x700) >> 8; + initialpage = hashpage&0xFF; + startTeletext(); + } + } + + updateHashstring(); +} + +function loadChannelImage(imgURL, imgRefresh){ + clearInterval(imageRefreshTimeout); + let cm = channelMedia; + const u = new URL(imgURL); + if (u.href.startsWith("data:image/")){ + // allow data urls + videoImage.channelMedia = cm; + videoImage.src = imgURL; + } else if (u.hostname.endsWith(new URL(websocket.url).hostname)){ + // allow requests to service host and subdomains only + fetch(imgURL, {redirect:"error"}) // require allow CORS, refuse any redirects + .then((response) => response.blob()) + .then((blob) => { + videoImage.channelMedia = cm; + videoImage.src = URL.createObjectURL(blob); + }); + if (imgRefresh){ + if (imgRefresh < 10) + imgRefresh = 10; // make minimum refresh interval 10 seconds + imageRefreshTimeout = setInterval(refreshImage, imgRefresh * 1000); + } + } else { + console.log("loading image from "+imgURL+" disallowed.") + } +} + +function refreshImage(){ + videoImage.src = videoImage.src.split("?")[0]+"?"+new Date().getTime(); +} + +function loadChannelVideo(vidURL){ + let cm = channelMedia; + const u = new URL(vidURL); + if (u.hostname.endsWith(new URL(websocket.url).hostname)){ + // allow requests to service host and subdomains only + fetch(vidURL, {redirect:"error"}) // require allow CORS, refuse any redirects + .then((response) => response.blob()) + .then((blob) => { + videoVideo.channelMedia = cm; + videoVideo.src = URL.createObjectURL(blob); + }); + } else { + console.log("loading video from "+vidURL+" disallowed.") + } +} + +function loadChannelAudio(audURL){ + let cm = channelMedia; + const u = new URL(audURL); + if (u.hostname.endsWith(new URL(websocket.url).hostname)){ + // allow requests to service host and subdomains only + fetch(audURL, {redirect:"error"}) // require allow CORS, refuse any redirects + .then((response) => response.blob()) + .then((blob) => { + videoAudio.channelMedia = cm; + videoAudio.src = URL.createObjectURL(blob); + }); + } else { + console.log("loading audio from "+vidURL+" disallowed.") + } +} + +function imageLoaded(){ + if (channelMedia == videoImage.channelMedia){ + staticEffect(false); + videoCanvasContext.fillStyle = "#000000"; + videoCanvasContext.fill(); + let iw = videoImage.width; + let ih = videoImage.height; + let is; + if ((iw / ih) > (4/3)) + is = (widescreen?1024:768) / iw; + else + is = 576 / ih; + let w = iw * is; + let h = ih * is; + let cx = (widescreen?1024:768) / 2; + videoCanvasContext.drawImage(videoImage,cx - (w / 2),288 - (h / 2),w,h); + } +} + +function videoLoaded(){ + if (channelMedia == videoVideo.channelMedia){ + videoCanvasContext.fillStyle = "#000000"; + videoCanvasContext.fill(); + videoVideo.currentTime = (new Date() / 1000) % Math.floor(videoVideo.duration); + videoVideo.play(); + } +} + +function audioLoaded(){ + if (channelMedia == videoAudio.channelMedia){ + videoAudio.currentTime = (new Date() / 1000) % Math.floor(videoAudio.duration); + videoAudio.play(); + } +} + +let settingReceived = false; + +function channelSettings(data){ + settingReceived = true; + + staticEffect(false); + videoCanvasContext.fillStyle = "#4d4dff"; + videoCanvasContext.fill(); + + // apply received channel settings + const channelHostname = new URL(websocket.url).hostname + + if (data.image){ + let u = new URL(data.image.url) + loadChannelImage(data.image.url, data.image.refresh); + } else { + clearInterval(imageRefreshTimeout); + } + + if (data.video){ + loadChannelVideo(data.video.url); + } else { + clearInterval(videoInterval); + videoVideo.pause(); + } + + if (data.audio){ + loadChannelAudio(data.audio.url); + } else { + videoAudio.pause(); + } + + if (data.failtext){ + failTextString = loadEditorHash(data.failtext.data); + let hashpage = parseInt(data.failtext.page, 16) + if ((hashpage < 0x100) || (hashpage > 0x8FF) || ((hashpage & 0xFF) == 0xFF) || isNaN(hashpage)) + failPageNumber = 0x8ff; + else + failPageNumber = hashpage; + } + + if (data.localCOP){ + if (data.localCOP.G0andG2) + localCOPG0andG2 = parseInt(data.localCOP.G0andG2,16); + if (data.localCOP.G0andNOS) + localCOPG0andNOS = parseInt(data.localCOP.G0andNOS,16); + } +} + +let base64dictionary = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; + +function loadEditorHash(hashString){ + let pageText = ""; + /* this code is essentially identical to the code in edit.tf because it implements the same decoding algorithm */ + let charcode = 0; + for (let strpos = 0; strpos < hashString.length; strpos++){ + let base64Digit = base64dictionary.indexOf(hashString.charAt(strpos)); + for (let hashBit = 0; hashBit < 6; hashBit++){ + let charBit = (strpos * 6 + hashBit) % 7 + let bitValue = base64Digit >> (5 - hashBit) & 0x01; + charcode |= bitValue << 6 - charBit; + if (charBit == 6){ + let character = [Math.floor((strpos * 6 + hashBit - charBit) / 7)]; + + let row = Math.floor(character / 40); + let column = character % 40; + + if (row > 22 && (charcode == 0x0D || charcode == 0x0F)) // strip out double height/size on rows 23 and 24 + charcode = 0x20; + if (column > 37 && (charcode == 0x0E || charcode == 0x0F)) // strip out double width/size on columns 38 and 38 + charcode = 0x20; + + pageText += String.fromCharCode(charcode); + + charcode = 0; + } + } + } + return pageText; +} + +function blitVideo(){ + let iw = videoVideo.videoWidth; + let ih = videoVideo.videoHeight; + let is + if ((iw / ih) == (5/4)){ // correct aspect ratio + iw = ih * (4/3); + } + if ((iw / ih) > (4/3)) + is = (widescreen?1024:768) / iw; + else + is = 576 / ih; + let w = iw * is; + let h = ih * is; + let cx = (widescreen?1024:768) / 2; + videoCanvasContext.drawImage(videoVideo,cx - (w / 2),288 - (h / 2),w,h); +} + +function overlayCharacter(y,x,charcode,colour,charset,nos,dh,dw){ + renderer.screenAttributesArray[y][x] = {switchG0:false, mosaics:false, dh:dh, dw:dw, br:0, rh:0, flash:0, charcode:charcode, charSet:charset, NOS:nos, foreColour:colour, backColour:0x20, diacritic:0, boxed:false, conceal:false, separated:false, underline:false, invert:false, bold:false, italic:false, proportional:false} + if (dw){ + renderer.screenAttributesArray[y][x+1] = {switchG0:false, mosaics:false, dh:dh, dw:dw, br:0, rh:1, flash:0, charcode:charcode, charSet:charset, NOS:nos, foreColour:colour, backColour:0x20, diacritic:0, boxed:false, conceal:false, separated:false, underline:false, invert:false, bold:false, italic:false, proportional:false} + } + if (dh){ + renderer.screenAttributesArray[y+1][x] = {switchG0:false, mosaics:false, dh:dh, dw:dw, br:1, rh:0, flash:0, charcode:charcode, charSet:charset, NOS:nos, foreColour:colour, backColour:0x20, diacritic:0, boxed:false, conceal:false, separated:false, underline:false, invert:false, bold:false, italic:false, proportional:false} + if (dw){ + renderer.screenAttributesArray[y+1][x+1] = {switchG0:false, mosaics:false, dh:dh, dw:dw, br:1, rh:1, flash:0, charcode:charcode, charSet:charset, NOS:nos, foreColour:colour, backColour:0x20, diacritic:0, boxed:false, conceal:false, separated:false, underline:false, invert:false, bold:false, italic:false, proportional:false} + } + } +} + +function clearOverlay(){ + overlayActive = false; + renderer.setTeletextOpacity(0); // hide teletext + renderer.initializeLevel1PageArray(); + renderer.packetX26TripletArray.length = 0; + renderer.resetPresentationData(); + renderer.decodePage(); +} + +function volumebuttonhandler(val){ + if (powerOnFlag){ + resetPowerOffTimeout(); + let curvol = videoVideo.volume; + let newvol = curvol; + let muted = videoVideo.muted; + + if (val == "mute"){ + videoVideo.muted = !videoVideo.muted; + videoAudio.muted = videoVideo.muted; + } else { + videoVideo.muted = false; + videoAudio.muted = false; + } + + if (val == "up") { + newvol = curvol + 0.025; + } else if (val == "down") + newvol = curvol - 0.025; + + if (newvol > 1) newvol = 1; + else if (newvol < 0) newvol = 0; + + videoVideo.volume = newvol; + videoAudio.volume = newvol; + setLocalStorage("videoMuted",videoVideo.muted); + setLocalStorage("videoVolume",videoVideo.volume); + + if (!teletextOnFlag){ + clearOverlay(); + renderer.setRenderLevel(3); + renderer.setStretchMode(0); + renderer.setMixMode(true); + renderer.hideTeletext(false); + renderer.numberEntry(false); + renderer.setDisplayRow24(true); + renderer.resetPageSettings(); + renderer.enableSidePanels(widescreen); + for (let i=0; i<40; i++){ + overlayCharacter(22,i,0x2D,2,0,0,true,false); + } + overlayCharacter(22,Math.floor(39 * newvol),0x7C,2,0,0,true,false); + + let str = " "; + if (videoVideo.muted){ + str = "Mute"; + } + + let o = widescreen?40:32; + for (let i = 0; i < 4; i++) + overlayCharacter(0,2*i+o,str.charCodeAt(i),2,0,0,true,true); + + overlayActive = true; + renderer.redrawScreen(); + renderer.setTeletextOpacity(1); // show teletext + clearTimeout(overlayTimeout); /* clear any previous timeout */ + overlayTimeout = setTimeout(clearOverlay, 3000); /* clear the overlay after 3 seconds */ + } + } +} + +function channelbuttonhandler(val){ + resetPowerOffTimeout(); + if (powerOnFlag){ + if (!teletextOnFlag){ + if (val == "up"){ + if (channelNumber < 9) + changeChannelByNumber(channelNumber + 1); + else + changeChannelByNumber(0); + } else if (val == "down"){ + if (channelNumber <= 0) + changeChannelByNumber(9); + else + changeChannelByNumber(channelNumber - 1); + } + } else { + teletextPageUpDown(val); + } + } + else + { + powerOnFlag = true; + changeChannelByNumber(channelNumber); + videoCanvasContext.drawImage(staticImg,0,0); + staticEffect(true); + renderer.enable(true); + } +} + +function teletextPageUpDown(val){ + /* teletext page up/down */ + let curpage = magazine << 8 | page; + let existpage; + + if (val == "up"){ + existpage = pageExists.find(element => element > curpage); + if (!existpage) + existpage = pageExists[0]; + + if (!existpage){ + /* no pageExists data */ + existpage = curpage + 1; + if ((existpage & 0xF) > 9) + existpage = (existpage & 0x7F0) + 0x10; + if ((existpage & 0xF0) > 0x90) + existpage = (existpage & 0x70F) + 0x100; + if (existpage >= 0x800) + existpage = 0x0; + } + } else if (val == "down"){ + existpage = revPageExists.find(element => element < curpage); + if (!existpage) + existpage = revPageExists[0]; + + if (!existpage){ + /* no pageExists data */ + existpage = curpage - 1; + if (existpage < 0) + existpage = 0x799; + if ((existpage & 0xF0) > 0x90) + existpage = (existpage & 0x70F) | 0x90; + if ((existpage & 0xF) > 9) + existpage = (existpage & 0x7F0) | 0x9; + } + } else { + return; + } + + magazine = existpage >> 8; + page = existpage & 0xFF; + subcode = 0x3F7F; + level1PageArray[0][7] = tvsearchcolour; /* set hardware colour */ + pagesearch = true; + pagehold = false; + numberentry = false; + slots[0] = {m:magazine,p:page,s:subcode,e:false}; + websocketSend(["pagesearch", 0, magazine, page, subcode, true, false]); /* send desired number to server with search slot zero */ + resetnumber(); + redrawPage(); +} + +function powerbuttonhandler(){ + if (!powerOnFlag){ + resetPowerOffTimeout(); + powerOnFlag = true; + if (!noChannel) + changeChannelByNumber(channelNumber) + else + changeChannelByID(channelID); + videoCanvasContext.drawImage(staticImg,0,0); + staticEffect(true); + renderer.enable(true); + } else { + powerOnFlag = false; + staticEffect(false); + videoCanvasContext.rect(0, 0, 768, 576); + videoCanvasContext.fillStyle = "black"; + videoCanvasContext.fill(); // blank image + clearOverlay(); + if (teletextOnFlag) + stopTeletext(); + videoVideo.pause(); + videoAudio.pause(); + clearInterval(videoInterval); + websocketDisconnect(); + renderer.enable(false); + } +} + +function textbuttonhandler(){ + if (powerOnFlag){ + resetPowerOffTimeout(); + if (!teletextOnFlag){ + clearOverlay(); + clearTimeout(overlayTimeout); + startTeletext(); + } else if (teletextOnFlag && cancel) { + /* teletext is on but hidden - unhide */ + pageentrytimedout(); /* interrupt page entry */ + cancel = false; + resetnumber(); + redrawPage(); + } else { + stopTeletext(); + } + } +} + +function numberButtonHandler(number){ + resetPowerOffTimeout(); + if (powerOnFlag){ + if (teletextOnFlag) + teletextnumberbuttonhandler(number); + else { + changeChannelByNumber(number); + } + } + else + { + powerOnFlag = true; + changeChannelByNumber(number); + videoCanvasContext.drawImage(staticImg,0,0); + staticEffect(true); + renderer.enable(true); + } +} + +function mixbuttonhandler(){ + if (teletextOnFlag){ + resetPowerOffTimeout(); + mix = !mix; + renderer.setMixMode(mix); + } +} + +function sizebuttonhandler(){ + if (teletextOnFlag){ + resetPowerOffTimeout(); + size = (size + 1) % 3; + renderer.setStretchMode(size); + } +} + +function revealbuttonhandler(){ + if (teletextOnFlag){ + resetPowerOffTimeout(); + reveal = !reveal; + renderer.setReveal(reveal); + renderer.decodePage(); + renderer.redrawScreen(); + } +} + +function cancelbuttonhandler() { + /* hide button */ + if (teletextOnFlag){ + resetPowerOffTimeout(); + pageentrytimedout(); /* interrupt page entry */ + if (cancel){ + /* teletext is hidden - unhide */ + cancel = false; + } else { + /* teletext is visible - hide */ + cancel = true; + } + resetnumber(); + redrawPage(); + } +} + +function stopbuttonhandler(){ + /* hold button */ + if (teletextOnFlag){ + resetPowerOffTimeout(); + clearTimeout(numberentrytimer); + if(pagehold) { + pagehold = false; + if (pagesearch){ + level1PageArray[0][7] = tvsearchcolour; + } else { + level1PageArray[0][7] = 0x07; + } + } else { + pagehold = true; + level1PageArray[0][7] = 0x07; // white header + } + websocketSend(["hold",pagehold]); + timeentry = false; + resetnumber(); + redrawPage(); + } +} + +function linkbuttonhandler(link){ + let ch; + let linksubcodeandmags; + if (teletextOnFlag){ + resetPowerOffTimeout(); + if (linksFLOFMode){ + let linkpacketmagazine = linksArray[12]; + let linkpage = linksArray[link*2]; + linksubcodeandmags = linksArray[link*2+1]; + if (!((linkpage == 0xFF) && (linksubcodeandmags & 0x3F7F == 0x3F7F))) + { + /* link is not FF:3F7F */ + /* get magazine */ + let m1 = (linksubcodeandmags & 0x0080) >> 7; + let m2 = (linksubcodeandmags & 0x4000) >> 14; + let m3 = (linksubcodeandmags & 0x8000) >> 15; + magazine = linkpacketmagazine ^ ((m3<<2)+(m2<<1)+m1); + + subcode = linksubcodeandmags & 0x3F7F; + page = linkpage; + + level1PageArray[0][7] = tvsearchcolour; /* set hardware colour */ + pagesearch = true; + pagehold = false; + slots[0] = {m:magazine,p:page,s:subcode,e:false}; + websocketSend(["pagesearch", 0, magazine, page, subcode, true, false]); /* send desired number to server with search slot zero */ + resetnumber(); + redrawPage(); + } + } else if (linksTOPMode){ + // TODO TOP + } else { + // TODO "Cycle trace table" + if (link == 0) + teletextPageUpDown("down"); + else if (link == 1) + teletextPageUpDown("up"); + } + } +} + + + +function timetextbuttonhandler(){ + if (teletextOnFlag){ + resetPowerOffTimeout(); + cancel = false; + if (!timeentry){ + timeentry = true; + resetnumber(); + numberentrytimer = setTimeout(pageentrytimedout, 6000); /* wait 6 seconds */ + } else { + pageentrytimedout(); + } + } +} + +function teletextnumberbuttonhandler(num){ + if (teletextOnFlag){ + if (timeentry){ + switch (enterdigit){ + case 0: + if (num < 3){ + clearTimeout(numberentrytimer); + level1PageArray[0][1] = num + 0x30; + numberentry = true; + nextsubcode = num << 12; + enterdigit++; + numberentrytimer = setTimeout(pageentrytimedout, 6000); /* wait 6 seconds */ + redrawPage(); + } + break; + case 1: + clearTimeout(numberentrytimer); + level1PageArray[0][2] = num + 0x30; + nextsubcode = nextsubcode | (num << 8); + enterdigit++; + numberentrytimer = setTimeout(pageentrytimedout, 6000); /* wait 6 seconds */ + redrawPage(); + break; + case 2: + if (num < 8){ + clearTimeout(numberentrytimer); + level1PageArray[0][3] = num + 0x30; + nextsubcode = nextsubcode | (num << 4); + enterdigit++; + numberentrytimer = setTimeout(pageentrytimedout, 6000); /* wait 6 seconds */ + redrawPage(); + } + break; + case 3: + clearTimeout(numberentrytimer); + level1PageArray[0][4] = num + 0x30; + enterdigit = 0; + subcode = nextsubcode | num; + level1PageArray[0][7] = tvsearchcolour; /* set hardware colour */ + pagesearch = true; + numberentry = false; + timeentry = false; + slots[0] = {m:magazine,p:page,s:subcode,e:false}; + websocketSend(["pagesearch", 0, magazine, page, subcode, true, false]); /* send desired number to server with search slot zero */ + pagehold = false; + redrawPage(); + break; + } + } else { + switch (enterdigit){ + case 0: + cancel = false; + if ((num > 0) & (num < 9)){ + level1PageArray[0][1] = 0x50; + clearTimeout(numberentrytimer); + level1PageArray[0][2] = num + 0x30; + level1PageArray[0][3] = 0x2D; + level1PageArray[0][4] = 0x2D; + numberentry = true; + nextmagazine = num & 7; + enterdigit++; + numberentrytimer = setTimeout(pageentrytimedout, 6000); /* wait 6 seconds */ + redrawPage(); + } + break; + case 1: + clearTimeout(numberentrytimer); + level1PageArray[0][3] = num + 0x30; + nextpage = num << 4; + enterdigit++; + numberentrytimer = setTimeout(pageentrytimedout, 6000); /* wait 6 seconds */ + redrawPage(); + break; + case 2: + clearTimeout(numberentrytimer); + level1PageArray[0][4] = num + 0x30; + enterdigit = 0; + magazine = nextmagazine; + page = nextpage + num; + subcode = 0x3F7F; + level1PageArray[0][7] = tvsearchcolour; /* set hardware colour */ + pagesearch = true; + numberentry = false; + slots[0] = {m:magazine,p:page,s:subcode,e:false}; + websocketSend(["pagesearch", 0, magazine, page, subcode, true, false]); /* send desired number to server with search slot zero */ + pagehold = false; + redrawPage(); + break; + } + } + } +} + +function resetnumber(){ + if (teletextOnFlag){ + enterdigit = 0; + if(timeentry){ + level1PageArray[0].splice(1, 4, 0x2A, 0x2A, 0x2A, 0x2A ); /* change page number to **** */ + } else if(pagehold){ + level1PageArray[0].splice(1, 4, 0x53, 0x54, 0x4F, 0x50 ); /* change page number to STOP */ + } else if (cancel && pagesearch){ + level1PageArray[0][1] = 0x08; + level1PageArray[0][2] = 0x2A; + level1PageArray[0][3] = 0x2A; + level1PageArray[0][4] = 0x09; + } else if (subcode == 0x3F7F){ + level1PageArray[0][1] = 0x50; + if (!magazine) + level1PageArray[0][2] = 0x38; + else + level1PageArray[0][2] = magazine + 0x30; + + let ch=page>>4; // page tens + level1PageArray[0][3]=ch+(ch>9?0x37:0x30); + ch=page%0x10; // page units + level1PageArray[0][4]=ch+(ch>9?0x37:0x30); + } else { + let ch=(subcode>>12)&0xF; + level1PageArray[0][1] = ch+(ch>9?0x37:0x30); + ch=(subcode>>8)&0xF; + level1PageArray[0][2] = ch+(ch>9?0x37:0x30); + ch=(subcode>>4)&0xF; + level1PageArray[0][3] = ch+(ch>9?0x37:0x30); + ch=subcode&0xF; + level1PageArray[0][4] = ch+(ch>9?0x37:0x30); + } + renderer.numberEntry(numberentry); + } +} + +function pageentrytimedout(){ + clearTimeout(numberentrytimer); /* clear timeout if this function gets called directly */ + if (teletextOnFlag){ + numberentry = false; + timeentry = false; + resetnumber(); + redrawPage(); + } +} + +function resetPagePresentationData(presentationData){ + presentationData.defaultG0andG2 = localCOPG0andG2; + presentationData.secondG0andNOS = localCOPG0andNOS; + presentationData.lsp = false; + presentationData.rsp = false; + presentationData.spsf = true; + presentationData.cols = 0; + presentationData.dsc = 0; + presentationData.drc = 0; + presentationData.bbcs = false; + presentationData.ctm = 0; + presentationData.clut = []; + for (let i=0;i<0x20;i++) + presentationData.clut[i] = renderer.getDefaultCLUTEntry(i); +} + +function resetMagazinePresentationData(){ + for (let mag=0; mag<8; mag++){ + magazinePresentationData[mag] = {}; + magazineHasX290[mag] = false; + resetPagePresentationData(magazinePresentationData[mag]); + } +} + +function initializeMOTPageArray(){ + for (let mag=0; mag<8; mag++){ + MOTPageArray[mag] = {lastpacket:0,packets:[],pages:[],pop2:[],drcs2:[],pop3:[],drcs3:[]}; + let m = MOTPageArray[mag]; + for (let p = 0; p < 0x100; p++){ + m.pages[p] = {gpop:0,pop:0,gdrcs:0,drcs:0}; + } + for (let i=0; i<8; i++){ + m.pop2[i] = {lf:false, m:0, p:0xff, s:0, ff:1, dof:0, do1:0, do2:0}; + m.pop3[i] = {lf:false, m:0, p:0xff, s:0, ff:1, dof:0, do1:0, do2:0}; + m.drcs2[i] = {lf:false, m:0, p:0xff, s:0}; + m.drcs3[i] = {lf:false, m:0, p:0xff, s:0}; + } + } +} + +function clearLinkArray(mag){ + for (let i = 0; i < 5; i++) + { + linksArray[2*i] = 0xff; + linksArray[2*i+1] = 0x3f7f; + } + linksArray[10] = initialpage; + let magbits = (mag ^ initialmagazine); + linksArray[11] = 0x3F7F | ((magbits & 1) << 7) | ((magbits & 6) << 13); + linksArray[12] = mag; +} + +function startTeletext(){ + renderer.setRenderLevel(renderLevel); + renderer.setStretchMode(0); + renderer.setMixMode(false); + cancel = false; + mix = false; + size = 0; + reveal = false; + pagehold = false; + numberentry = false; + timeentry = false; + renderer.numberEntry(numberentry); + enterdigit = 0; + magazine = initialmagazine; + page = initialpage; + subcode = initialsubcode; + nextcontrolbits = 0; + renderer.initializeLevel1PageArray(); + renderer.resetPresentationData(); + renderer.packetX26TripletArray.length = 0; + for (let r=0;r<26;r++){ + level1PageArray[r] = []; + for (let c=0;c<40;c++){ + level1PageArray[r][c] = 0x20; + } + } + let CCTlabel = "\x01\x1d\x00\- \x02\x1d\x00+ \x1c" + for (let c=0; c<12; c++){ + level1PageArray[24][c] = CCTlabel.charCodeAt(c); + } + packetX26TripletArray.length = 0; + resetPagePresentationData(pagePresentationData); + resetMagazinePresentationData(); + clearLinkArray(initialmagazine); + linksFLOFMode = false; + linksTOPMode = false; + initializeMOTPageArray(); + enhancementPageCache.length = 0; + pageExists.length = 0; + revPageExists.length = 0; + level1PageArray[0].splice(0, 7, tvnumbercolour, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20 ); /* white page number */ + level1PageArray[0][7] = tvsearchcolour; + teletextOnFlag = true; + resetnumber(); + slots.length = 0; // clear slots + if (websocketFailedFlag){ + failPage(); + } else { + websocketSend(["ttx", true]); // turn on + websocketSend(["pagesearch",0,magazine,page,subcode,true,false,false]); /* send desired number to server with search slot zero */ + slots[0] = {m:magazine,p:page,s:subcode,e:false}; + for (let i=1; i<=8; i++){ + slots[i] = {m:i,p:0xFE,s:0,e:true}; // MOT searchslots (1-8) + } + slots[9] = {m:1,p:0xF0,s:0x3F7F,e:false}; // Basic TOP table (searchslot 9) + slots[10] = {m:1,p:0xF1,s:0,e:false}; // TOP multiplage table (searchslot 10) + slots[11] = {m:1,p:0xF2,s:0,e:false}; // TOP additional information table 1 + slots[12] = {m:1,p:0xF3,s:0,e:false}; // TOP additional information table 2 + for (let i=1; i> 12; + renderer.pagePresentationData.G0andG2 = (pagePresentationData.defaultG0andG2 & 0x78) | ((NOSbits & 1) << 2) | (NOSbits & 2) | ((NOSbits & 4) >> 2); + renderer.pagePresentationData.G0andNOS = pagePresentationData.secondG0andNOS; + renderer.pagePresentationData.lsp = pagePresentationData.lsp; + renderer.pagePresentationData.rsp = pagePresentationData.rsp; + renderer.pagePresentationData.spsf = pagePresentationData.spsf; + renderer.pagePresentationData.cols = pagePresentationData.cols; + renderer.pagePresentationData.dsc = pagePresentationData.dsc; + renderer.pagePresentationData.drc = pagePresentationData.drc; + renderer.pagePresentationData.bbcs = pagePresentationData.bbcs; + renderer.pagePresentationData.ctm = pagePresentationData.ctm; + renderer.pagePresentationData.clut = pagePresentationData.clut.slice(); + + for (let r=0;r<26;r++){ + for (let c=0;c<40;c++){ + renderer.level1PageArray[r][c] = level1PageArray[r][c]; + } + } + renderer.packetX26TripletArray.length = 0; + for (let i=0; i= 24){ + gpopLink = MOTPageArray[packetMagazine].pop3[0]; + popLink = MOTPageArray[packetMagazine].pop3[pop]; + gdrcsLink = MOTPageArray[packetMagazine].drcs3[0]; + drcsLink = MOTPageArray[packetMagazine].drcs3[drcs]; + } + + renderer.resetDRCSArrays(); + renderer.resetPOPArrays(); + + if (gpop) + { + renderer.defaultObjects[0].flag = gpopLink.dof & 0x3; + renderer.defaultObjects[0].subpage = gpopLink.d01 & 0xf; + renderer.defaultObjects[0].object = gpopLink.d01 & 0xf; + + renderer.defaultObjects[1].flag = (gpopLink.dof>>2) & 0x3; + renderer.defaultObjects[1].subpage = (gpopLink.do1>>4) & 0xf; + renderer.defaultObjects[1].object = (gpopLink.do2>>4) & 0xf; + + let cache = needEnhancementPage(0, gpopLink.m, gpopLink.p, 0x3f7f, gpopLink.s); + popLoadFromCache(cache, true); + } + if (pop) + { + renderer.defaultObjects[2].flag = popLink.dof & 0x3; + renderer.defaultObjects[2].subpage = popLink.d01 & 0xf; + renderer.defaultObjects[2].object = popLink.d01 & 0xf; + + renderer.defaultObjects[3].flag = (popLink.dof>>2) & 0x3; + renderer.defaultObjects[3].subpage = (popLink.do1>>4) & 0xf; + renderer.defaultObjects[3].object = (popLink.do2>>4) & 0xf; + + let cache = needEnhancementPage(1, popLink.m, popLink.p, 0x3f7f,popLink.s); + popLoadFromCache(cache, false); + } + if (gdrcs){ + let cache = needEnhancementPage(2, gdrcsLink.m, gdrcsLink.p, 0x3f7f, gdrcsLink.s); + drcsLoadFromCache(cache, true); + } + if (drcs){ + let cache = needEnhancementPage(3, drcsLink.m, drcsLink.p, 0x3f7f, drcsLink.s); + drcsLoadFromCache(cache, false); + } + } else { + needRedrawFlag = true; + } + + } else { + if (slots[searchslot].p == 0xFE){ + // MOT pages + MOTPageArray[packetMagazine].lastpacket = (packetSubcode & 0x3f00) >> 8; + + // TODO: proper implementation of Annex B.7 + if (controlbits & ERASE) + MOTPageArray[packetMagazine].packets.length = 0; // clear old packets + } else if (slots[searchslot].e){ + // other enhancement page + if (!(enhancementPageCache[(slots[searchslot].m << 8) | slots[searchslot].p])){ + enhancementPageCache[(slots[searchslot].m << 8) | slots[searchslot].p] = {s:[]}; + } + slots[searchslot].ps = packetSubcode & 0xF; + // TODO: proper implementation of Annex B.7 + if (controlbits & ERASE) + enhancementPageCache[(slots[searchslot].m << 8) | slots[searchslot].p].s[slots[searchslot].ps] = {r:[]}; // clear rows + } else if (searchslot == 9){ + // Basic TOP Table slot + console.log("header received for basic TOP table "+(((slots[searchslot].m)<<8) | slots[searchslot].p).toString(16)); + websocketSend(["hold",true,searchslot]); // TODO: should keep checking this for updates but freeze it until implemented + } else if (searchslot >= 10 && searchslot <= 12){ + // other TOP tables + console.log("header received for TOP page "+(((slots[searchslot].m)<<8) | slots[searchslot].p).toString(16)); + websocketSend(["hold",true,searchslot]); // freeze acquisition of this page until Basic TOP table is changed + // TODO should really do this once the page has been received and decoded correctly + } else { + console.log("header received for page "+(((slots[searchslot].m)<<8) | slots[searchslot].p).toString(16))+" on search slot "+searchslot; + } + } + } + break; + case "row": + let packetMagazine = message[2]; + let packetnumber = message[3]; + let packetstring = atob(message[4]); + let packet = []; + for (let i=0; i 0){ + // TODO: switch this to storing in same array as 0-25 for object pages + let decoded = decodeEnhancementPacket(packet); + + if (decoded[0] == nextX26DC){ + nextX26DC++; + for (let i=0;i<13;i++){ + packetX26TripletArray.push(decoded[1+i]); + } + } else { + packetX26TripletArray.length = 0; + nextX26DC = 0; + } + } else if (packetnumber == 27){ + let dc = hamming_8_4_decode(packet[0]); + if (dc == 0){ + // X/27/0 packet + + for (let i=0;i<6;i++){ + let l1 = hamming_8_4_decode(packet[6*i+1])&0xF; + let l2 = hamming_8_4_decode(packet[6*i+2])&0xF; + let l3 = hamming_8_4_decode(packet[6*i+3])&0xF; + let l4 = hamming_8_4_decode(packet[6*i+4])&0xF; + let l5 = hamming_8_4_decode(packet[6*i+5])&0xF; + let l6 = hamming_8_4_decode(packet[6*i+6])&0xF; + linksArray[2*i] = l2*0x10 + l1; //page number + linksArray[2*i+1] = (l3 | (l4 << 4) | (l5 << 8) | (l6 << 12)); // subcode and mag bits + } + linksArray[12] = packetMagazine; + linksFLOFMode = true; + + renderer.setDisplayRow24((hamming_8_4_decode(packet[37]) & 0x8) >> 3); + } else if (dc == 4 || dc == 5){ + let decoded = decodeEnhancementPacket(packet); + // X/27/4 or X/27/5 packet + let lf,pv,pu,pt,sf,valid; + for (let i=0; i<((dc==4)?6:2); i++){ + lf = (decoded[0]==4 && i<4)?i:(decoded[2*i+1] & 3); + pv = (decoded[2*i+1] >> 2) & 3; + pu = (decoded[2*i+1] >> 6) & 0xf; + mag = packetMagazine ^ ((decoded[2*i+1] >> 11) & 7); + pt = (decoded[2*i+1] >> 14) & 0xf; + sf = decoded[2*i+2] >> 2; + + if (renderLevel==2){ + if (i>4 || dc==5){ + valid = false; + } else { + valid = Boolean(pv & 1); + } + } else if (renderLevel==3){ + valid = Boolean(pv & 2); + } + + if (valid){ + for (let i=0;i<16;i++){ + if (sf & (1< 1){ + let decoded = decodeEnhancementPacket(packet); + + if (decoded[0] == 0){ + decodePacket28(decoded); + haveX280 = true; + } else if (decoded[0] == 4){ + decodePacket28(decoded); + } + } else if (packetnumber == 29 && renderLevel > 1){ + let decoded = decodeEnhancementPacket(packet); + if (decoded[0] == 0){ + decodePacket29(decoded, packetMagazine); + magazineHasX290[packetMagazine] = true; + } else if (decoded[0] == 4){ + decodePacket29(decoded, packetMagazine); + } + } else { + console.log(message); + } + } else if (slots[searchslot].p == 0xFE){ + // MOT pages + if (packetnumber < 25){ + let motpacket = {n:packetnumber,d:[]}; + for (let i=0;i<40;i++){ + motpacket.d[i] = hamming_8_4_decode(packet[i]); + } + MOTPageArray[packetMagazine].packets.push(motpacket); + } + if (packetnumber == MOTPageArray[packetMagazine].lastpacket){ + // clear page association array to defaults + for (let p = 0; p < 0x100; p++){ + MOTPageArray[packetMagazine].pages[p] = {gpop:0,pop:0,gdrcs:0,drcs:0}; + } + + // decode packets into page association array + for (let p = 0; p < MOTPageArray[packetMagazine].packets.length; p++){ + let packets = MOTPageArray[packetMagazine].packets[p]; + let n = MOTPageArray[packetMagazine].packets[p].n; + if (n < 9){ + // groups of decimal units (00-19 .. E0-F9) + for (let i=0;i<2;i++){ + for (let j=0;j<10;j++){ + let pop = packets.d[(i*20)+(j*2)]; + let drcs = packets.d[(i*20)+(j*2)+1]; + let pg = ((n-1)*0x20)+(i*0x10)+j; + MOTPageArray[packetMagazine].pages[pg].gpop = (pop >> 3) & 1; + MOTPageArray[packetMagazine].pages[pg].pop = pop & 7; + MOTPageArray[packetMagazine].pages[pg].gdrcs = (drcs >> 3) & 1; + MOTPageArray[packetMagazine].pages[pg].drcs = drcs & 7; + } + } + } else if (n < 15){ + // groups of hex units (0A-2F ... FA-FF) + for (let i=0;i<((n==14)?1:3);i++){ + for (let j=0;j<6;j++){ + let pop = packets.d[(i*12)+(j*2)]; + let drcs = packets.d[(i*12)+(j*2)+1]; + let pg = ((n-9)*0x30)+(i*0x10)+0x0A+j; + MOTPageArray[packetMagazine].pages[pg].gpop = (pop >> 3) & 1; + MOTPageArray[packetMagazine].pages[pg].pop = pop & 7; + MOTPageArray[packetMagazine].pages[pg].gdrcs = (drcs >> 3) & 1; + MOTPageArray[packetMagazine].pages[pg].drcs = drcs & 7; + } + } + } else if (n >= 19 && n <= 24){ + if (n != 21 && n != 24){ + for (let i=0; i<4; i++){ + let popObj = (n<22)?MOTPageArray[packetMagazine].pop2[(n==20)?i+4:i]:MOTPageArray[packetMagazine].pop3[(n==23)?i+4:i]; + popObj.lf=Boolean(packets.d[i*10]&8); + popObj.m=packets.d[i*10]&7; + popObj.p=((packets.d[i*10+1]&0xf) << 4) | (packets.d[i*10+2]&0xf); + popObj.s=packets.d[i*10+3]&0xf; + popObj.ff=(packets.d[i*10+4]&0xf); + popObj.dof=(packets.d[i*10+5]&0xf); + popObj.do1=((packets.d[i*10+6]&0xf) << 4) | (packets.d[i*10+7]&0xf); + popObj.do2=((packets.d[i*10+8]&0xf) << 4) | (packets.d[i*10+9]&0xf); + } + } else { + for (let i=0; i<8; i++){ + let drcsObj = (n==21)?MOTPageArray[packetMagazine].drcs2[i]:MOTPageArray[packetMagazine].drcs3[i]; + drcsObj.lf=Boolean(packets.d[i*4]&8); + drcsObj.m=packets.d[i*4]&7; + drcsObj.p=((packets.d[i*4+1]&0xf) << 4) | (packets.d[i*4+2]&0xf); + drcsObj.s=packets.d[i*4+3]&0xf; + } + // TODO: number of enhancement pages + } + } + } + + MOTPageArray[packetMagazine].packets.length = 0; // clear old packets for next time + // TODO: implement Annex B.6 properly + + let popArray, drcsArray; + + if ((renderLevel == 3) && (MOTPageArray[packetMagazine].lastpacket == 24)){ + popArray = MOTPageArray[packetMagazine].pop3; + drcsArray = MOTPageArray[packetMagazine].drcs3; + } else { + popArray = MOTPageArray[packetMagazine].pop2; + drcsArray = MOTPageArray[packetMagazine].drcs2; + } + + for (let i=0; i<8; i++){ + if (popArray[i].p != 0xff) + needEnhancementPage((i?1:0),popArray[i].m,popArray[i].p,0x3F7F,popArray[i].c); + if (drcsArray[i].p != 0xff) + needEnhancementPage((i?3:2),drcsArray[i].m,drcsArray[i].p,0x3F7F,drcsArray[i].c); + } + } + } else if (slots[searchslot].e){ + // searchslots for downloading enhancement pages + let epage = enhancementPageCache[(slots[searchslot].m << 8) | slots[searchslot].p]; + let esubcode = slots[searchslot].ps; + if (!epage.s[esubcode]) + epage.s[esubcode] = {r:[]}; + if (packetnumber < 26){ + epage.s[esubcode].r[packetnumber - 1] = packet; + } else if (packetnumber == 26){ + let dc = hamming_8_4_decode(packet[0]); + if (dc != 0xff){ + epage.s[esubcode].r[25 + dc] = packet; + } + } + } else { + // something else + } + } + break; + case "initialpage": + if (!settingReceived) // got bdsp initial page before channel settings, means old server + channelSettings({}); // set some default channel settings + + initialmagazine = message[2]; + initialpage = message[3]; + initialsubcode = message[4]; + if (overlayActive){ + let statusDisplay = atob(message[5]); + renderer.enableSidePanels(widescreen); + if (widescreen){ + for (let c = 0; c < 4; c++){ + overlayCharacter(2,64 + 2*c,statusDisplay.charCodeAt(c) & 0x7f,2,0,0,true,true); + } + for (let c = 0; c < 16; c++){ + overlayCharacter(2,2*c,statusDisplay.charCodeAt(c+4) & 0x7f,2,0,0,true,true); + } + } else { + for (let c = 0; c < 20; c++){ + overlayCharacter(2,2*c,statusDisplay.charCodeAt(c) & 0x7f,2,0,0,true,true); + } + } + renderer.redrawScreen(); + } + break; + case "pageExists": + pageExists.push((message[2] << 8) | message[3]); + pageExists.sort(function(a, b){return a-b}); + revPageExists = pageExists.slice().reverse(); + break; + } +} + +function needEnhancementPage(type, mag, page, subcode, count){ + // type 0-3 is gpop,pop,gdrcs,drcs + // subcode is specific subpage to search for or 3F7F + // count is number of subpages pages to cache + + // debug + let string = ["gpop","pop","gdrcs","drcs"] + console.log(string[type]+" required "+((mag<<8)|page).toString(16).toUpperCase()+"/"+("000"+subcode.toString(16).toUpperCase()).slice(-4)); + + let searching = false; + for (let i=1; i> 1; + if (subtable.r[row]){ + let data = []; + for (let i=0; i<20; i++){ + let six = (subtable.r[row][(20*(ptu&1)) + i]) & 0x3F; + for (let j=0; j<6; j++){ + data[i*6+j] = (six & (0x20>>j))?1:0; + } + } + return data; + } else { + return null; + } +} + +function popLoadFromCache(cache, gpop){ + if (cache){ + for (let i=0; i<16; i++){ + if (cache.s[i]){ + for (let j=0; j<32; j++){ + let active = importObject(cache.s[i],j,0); + let adaptive = importObject(cache.s[i],j,1); + let passive = importObject(cache.s[i],j,2); + if (active || adaptive || passive){ + if (gpop) + renderer.gpopArray[(32*i)+j] = {active:active,adaptive:adaptive,passive:passive}; + else + renderer.popArray[(32*i)+j] = {active:active,adaptive:adaptive,passive:passive}; + } + } + } + } + } +} + +function importObject(subpage, object, type) +{ + /* type: 0=active, 1=adaptive, 2=passive */ + if (object > 31){ + console.log("importObject: invalid object number"); + return null; + } + let row = (object >> 3); + if (subpage.r[row]){ + let triplets = decodeEnhancementPacket(subpage.r[row]) + if (triplets[0] & 1){ // check if this is pointer data + let triplet = triplets[2 + (((object>>1)&3) * 3) + type]; + let pointer = (object&1)?(triplet>>9):(triplet&0x1FF); + if (pointer != 0x1FF && pointer <= 506){ + return getObjectData(subpage,pointer); + } else { + return null; // unused pointer + } + } else { + return null; // not a pointer row + } + } else { + return null; // row not present + } +} + +function getObjectData(subpage,pointer){ + let row = 2 + Math.floor(pointer / 13); + if (subpage.r[row]){ + let triplets = decodeEnhancementPacket(subpage.r[row]) + if ((row < 4) && (triplets[0]&1)){ + console.log("getObjectData: pointer to pointer table row"); + return null; + } else { + let triplet = (pointer % 13) + let objectData = []; + do{ + objectData.push(triplets[triplet+1]); + if ((objectData[objectData.length - 1]&0x7FF) == 0x7FF) + return objectData; // hit a object terminator + triplet++; + if (triplet > 12) + { + triplet = 0; + row++; + if (!(subpage.r[row])) + { + console.log("getObjectData: object data row missing"); + return null; + } + triplets = decodeEnhancementPacket(subpage.r[row]) + if ((row < 4) && (triplets[0]&1)){ + console.log("getObjectData: pointer table in row data"); + return null; + } + } + }while(1); + } + } else { + console.log("getObjectData: object data row missing"); + return null; + } +} + +function decodePacket28(data){ + decodeEnhancementCLUT(data, pagePresentationData); + + if (data[0] == 4 && haveX280) + return; // if X280 exists ignore rest of X284 + + pageFunction = data[1] & 0xF; + pageCoding = (data[1] >> 4) & 0x7; + + decodeEnhancement(data, pagePresentationData); +} + +function decodePacket29(data, magazine){ + decodeEnhancementCLUT(data, magazinePresentationData[magazine]); + + if (data[0] == 4 && magazineHasX290[magazine]) + return; // if X290 exists ignore rest of X294 + + if (data[1] & 0x7F) + return; // only packet function 0 valid + + decodeEnhancement(data, magazinePresentationData[magazine]); +} + +function decodeEnhancementCLUT(data, presentationData){ + let offset = (data[0] == 4)?0:0x10; + + for (let c = 0; c < 0x10; c++){ + let rtr = Math.floor(((c * 12) + 28) / 18)+1; + let rsh = Math.floor(((c * 12) + 28) % 18); + let r = (data[rtr] >> rsh) & 0xF; + if (rsh == 16) + r |= (data[rtr+1] & 3) << 2; + + let gtr = Math.floor(((c * 12) + 32) / 18)+1; + let gsh = Math.floor(((c * 12) + 32) % 18); + let g = (data[gtr] >> gsh) & 0xF; + if (gsh == 16) + g |= (data[gtr+1] & 3) << 2; + + let btr = Math.floor(((c * 12) + 36) / 18)+1; + let bsh = Math.floor(((c * 12) + 36) % 18); + let b = (data[btr] >> bsh) & 0xF; + if (bsh == 16) + b |= (data[btr+1] & 3) << 2; + + presentationData.clut[offset + c] = (r << 8) | (g << 4) | b; + } +} + +function decodeEnhancement(data, presentationData){ + presentationData.defaultG0andG2 = (data[1] >> 7) & 0x7F; + presentationData.secondG0andNOS = ((data[1] >> 14) & 0xF) | ((data[2] & 0x7) << 4); + presentationData.lsp = (data[2] >> 3) & 1; + presentationData.rsp = (data[2] >> 4) & 1; + presentationData.spsf = (data[2] >> 5) & 1; + presentationData.cols = (data[2] >> 6) & 0xF; + presentationData.dsc = (data[13] >> 4) & 0x1F; + presentationData.drc = (data[13] >> 9) & 0x1F; + presentationData.bbcs = (data[13] >> 14) & 1; + presentationData.ctm = (data[13] >> 15) & 7; +} + +/* wrappers around localstorage getItem and setItem */ +function getLocalStorage(key){ + try { + return localStorage.getItem("teletextViewer:"+key); + } + catch(e) { + console.log("Error: failed to get "+key+" from local storage"); + return null; + } +} + +function setLocalStorage(key, value){ + /* use "1" and "0" for true and false */ + if (value == false){ + localStorage.removeItem("teletextViewer:"+key,value); // remove keys when false + } else { + if (value == true) + value = "1"; + try { + localStorage.setItem("teletextViewer:"+key,value); + } + catch(e) { + console.log("Error: failed to set "+key+" in local storage"); + return; + } + } +} + +function updateStatic(){ + // magic numbers tweaked to make noise appear random-ish without either obvious periodic repeats or glitches from close random numbers + pseudoRandomXShift = (pseudoRandomXShift + 150 + ((60 * Math.random())|0)) % 422; + pseudoRandomYShift = (pseudoRandomYShift + 130 + ((60 * Math.random())|0)) % 576; + videoCanvasContext.drawImage(staticImg,pseudoRandomXShift,pseudoRandomYShift,768,576,0,0,widescreen?1024:768,576); +} + +function staticEffect(state){ + if (!state){ + clearInterval(staticTimer); + staticTimer = 0; + } else if (!staticTimer){ + staticTimer = window.setInterval( updateStatic, 40); + } +} + +/* all the global variables and arrays */ + +let level1PageArray = []; +let packetX26TripletArray = []; +let pagePresentationData = {}; +let linksFLOFMode; +let linksTOPMode; +let firstRowFlag; +let linksArray = []; /* somewhere to store link data */ +let magazinePresentationData = []; +let magazineHasX290 = []; +let MOTPageArray = []; +let enhancementPageCache = []; +let slots = []; // 0-8 are reserved +let pageExists = []; +let revPageExists = []; + +let powerOnFlag; +let paramsPage; +let teletextOnFlag; +let renderLevel; +let channelNumber; +let noChannel = false; +let channelID; +let mix; +let size; +let reveal; +let cancel; +let pagehold; +let magazine; +let page; +let subcode; +let controlbits; +let initialmagazine; +let initialpage; +let initialsubcode; +let numberentry; +let timeentry; +let numberentrytimer; +let enterdigit; +let nextmagazine; +let nextpage; +let nextsubcode; +let pagesearch; +let overlayActive; +let websocket = null; +let reconnectTimeout; +let staticTimer; +let pseudoRandomXShift = 0; +let pseudoRandomYShift = 0; +let nextcontrolbits; +let nextX26DC; +let haveX280; +let pageFunction; +let pageCoding; +let redrawOnNextHeader = false; +let hiddenTimeout; +let overlayTimeout; +let imageRefreshTimeout; +let videoImage; +let tvnumbercolour = 0x07; /* colour of page number */ +let tvsearchcolour = 0x02; /* hardware set colour when searching for page */ +let localCOPG0andG2; +let localCOPG0andNOS; + +let defaultFailTextData = "QIECBAgQIKmXZl6ZfHRBzy8u2Xkg35s2zTuyoECBAgQIECBAgQIECBAgQIECBAgQIECBAgQIECBAgQIECBAgQIECBAgQIAI2pFmRakWxUQVKUGdTmyadOTPnIECBAgQIECBAgQIECBAgQIECBAgQIECBAgQIECBAgQIECBAgQIECBAgQIECBAgQIECAksWLFixYsWLFixYsWLFixYsWLFixYsWLFiyhBjxQTBgtcuUCBAgQIECBAgQIECBAgQIECBAgQIECBAgQIECBAgQIECBAgI04tKtJhxQ8mdUi0qVWhUkz5yBAgQIECBAgQIECBAgQIECBAgQIECBAgQIECBAgQIECBAgQIECBAgQIECBAgQIECBAgQIEFfKgw8N-zfn088qDNv5IOmjTzQad3TLy5deHTTv3IOm9AgQdNGVBu38tuHYgk7s2_ltw9NO_cgp5eXbTjyoECBAgQIECBAgQIECBAgQIECBAgQIECBAgQIECBAgQIECBAgQIECBAgQIA1TRp5hemXn0QdOWHdz26efPTv3Bu-nZsQY9-7pp3dcqBAgDdd3TTsQdNGVBm67NiDnl5dtOPKgx4dyDFlQcsvPpv5Zci5AgQIECBAgQIECBAgQIECBAgQIECBAgQIECBAgQIECBAgQIEBdujRo0aNGjRo0aNGjRo0aNGjRo0aNGjRo0aNGjRo0aNqgGl2qAIdBoBipUqQIEA2pFp1EFCDHijECBAqVKkA1AgOF2qBAgQIECBAgQIECBAgQIECBAgQIECBAgQIECBAgQIECBAgQIEBdqgQIECBAgQIECBAgQIECBAgQIECBAgQIECBAgQIECBqgQF2qBAgQIEBoqsWLFixYsWLFixYsWLFixYsMl0CBAgQIGqBAXaoECBAgQIECBAgQIECBAgQIECBAgQIECBAgQIECBAgaoEBf____zxP____y3____kv____K____-R____8p____5dqgQF_____PE_____Lf___-S____8r____5H____yn____l2qBAX____88T____8t____5L____yv____kf____Kf___-XaoEBdGjRo0aNGjRo0aNGjRo0aNGjRo0aNGjRo0aNGjRo0aNCgQIECBAgQIECBAgQIECBAgQIECBAgQIECBAgQIECBAgQIECA";