@pixl-app/sdk 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/README.md +11 -1
- package/dist/pixl.js +4 -4
- package/package.json +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.1
|
|
4
|
+
|
|
5
|
+
Das Script nimmt die eigene Adresse nur noch als Server, wenn es von einer pixl-Instanz kommt (`/sdk/pixl.js`). Von jsDelivr, unpkg oder einem lokalen Dev-Server geladen, verbindet es sich mit pixl.lennox.zip statt mit dem CDN. `baseUrl` geht weiter vor, an `https://pixl.lennox.zip/sdk/pixl.js` ändert sich nichts.
|
|
6
|
+
|
|
3
7
|
## 0.1.0
|
|
4
8
|
|
|
5
9
|
Erste Version im eigenen Repo: Live-Events per Stream, `feed()` mit Verlauf und Live-Events ohne Doppelte, Chat mit Emotes, Spotify, StreamElements, TTS, persistente States und Einstellungen für eigene Widgets.
|
package/README.md
CHANGED
|
@@ -16,6 +16,12 @@ Per Script-Tag, ohne Installation:
|
|
|
16
16
|
</script>
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
Mit fester Version über jsDelivr. Unter `pixl.lennox.zip/sdk/pixl.js` kommt immer die Version, die pixl gerade ausliefert:
|
|
20
|
+
|
|
21
|
+
```html
|
|
22
|
+
<script src="https://cdn.jsdelivr.net/npm/@pixl-app/sdk@0.1.1/dist/pixl.js"></script>
|
|
23
|
+
```
|
|
24
|
+
|
|
19
25
|
Oder als Paket mit Bundler und TypeScript:
|
|
20
26
|
|
|
21
27
|
```bash
|
|
@@ -35,14 +41,18 @@ Den API-Schlüssel legst du in pixl unter „API & SDK“ an. Welche Felder jede
|
|
|
35
41
|
|
|
36
42
|
```bash
|
|
37
43
|
bun install
|
|
44
|
+
bun run dev # Dev-SDK unter http://localhost:4400/pixl.js, baut bei jeder Änderung neu
|
|
38
45
|
bun run check # Typen
|
|
39
46
|
bun run lint # Formatierung
|
|
47
|
+
bun test # Tests
|
|
40
48
|
bun run build # dist/pixl.js (Script), dazu dist/index.js und .d.ts
|
|
41
49
|
```
|
|
42
50
|
|
|
51
|
+
Ein Overlay mit dem Dev-SDK testen: `<script src="http://localhost:4400/pixl.js"></script>` einbinden. Es spricht dann mit pixl.lennox.zip, gegen einen lokalen pixl-Server zusätzlich `baseUrl` setzen. In einer OBS-Browserquelle oder einer lokalen HTML-Datei klappt das direkt, eine https-Seite wie StreamElements sperrt `http://localhost` unter Umständen.
|
|
52
|
+
|
|
43
53
|
Die Typen der pixl-API liegen als Kopie aus dem pixl-Monorepo in `src/api-types/`, bis es dafür ein npm-Paket gibt. Geändert werden sie dort, nicht hier.
|
|
44
54
|
|
|
45
|
-
Gegen eine lokale pixl-Instanz
|
|
55
|
+
Gegen eine lokale pixl-Instanz: `new Pixl({ key, baseUrl: "http://localhost:5173" })`.
|
|
46
56
|
|
|
47
57
|
## Versionen und Releases
|
|
48
58
|
|
package/dist/pixl.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
(()=>{function
|
|
1
|
+
(()=>{function S(t,e=1){let r=encodeURIComponent(t.id);switch(t.provider){case"twitch":return`https://static-cdn.jtvnw.net/emoticons/v2/${r}/${t.animated?"default":"static"}/dark/${e===4?3:e}.0`;case"7tv":return`https://cdn.7tv.app/emote/${r}/${e}x.webp`;case"bttv":return`https://cdn.betterttv.net/emote/${r}/${e===4?3:e}x.webp`;case"ffz":return`https://cdn.frankerfacez.com/emote/${r}/${e}`}}var M={"&":"&","<":"<",">":">",'"':""","'":"'"};function d(t){return t.replace(/[&<>"']/g,(e)=>M[e])}function b(t,e){let r=d(t.name);return`<img class="pixl-emote" src="${d(S(t,e))}" alt="${r}" data-provider="${t.provider}" data-emote="${d(t.id)}">`}function f(t,e={},r={}){let n=r.size??2,s=t.split(/(\s+)/),i=[],o=-1;for(let a of s){if(!a)continue;if(/^\s+$/.test(a)){i.push(d(a));continue}let l=Object.prototype.hasOwnProperty.call(e,a)?e[a]:void 0;if(!l){i.push(d(a)),o=-1;continue}if(l.zeroWidth&&o>=0){i.length=o+1;let u=i[o],c=u.startsWith('<span class="pixl-emote-stack">')?u.slice(31,-7):u;i[o]=`<span class="pixl-emote-stack">${c}${b(l,n)}</span>`;continue}i.push(b(l,n)),o=i.length-1}return i.join("")}function j(t=[]){return t.map((e)=>`<img class="pixl-badge" src="${d(e.url)}" alt="${d(e.title)}" data-provider="${e.provider}">`).join("")}var _=`.pixl-emote{height:1.75em;vertical-align:middle;margin:-.25em 0}
|
|
2
2
|
.pixl-emote-stack{display:inline-grid;vertical-align:middle}
|
|
3
3
|
.pixl-emote-stack>.pixl-emote{grid-area:1/1;justify-self:center;align-self:center;margin:0}
|
|
4
|
-
.pixl-badge{height:1.1em;vertical-align:middle;margin-right:.25em}`;function _(t){switch(t.type){case"linear-gradient":case"radial-gradient":{let e=t.stops.map((s)=>`${s.color} ${Math.round(s.at*1000)/10}%`).join(", "),r=`${t.repeating?"repeating-":""}${t.type}`;return t.type==="linear-gradient"?`${r}(${t.angle}deg, ${e})`:`${r}(${t.shape}, ${e})`}case"color":return`linear-gradient(${t.color}, ${t.color})`;case"image":return`url("${t.url.replace(/"/g,"%22")}")`}}function w(t,e="inherit"){let r=t.layers.filter((n)=>n.opacity>0);if(!r.length)return`color:${e}`;let s=t.shadows.map((n)=>`drop-shadow(${n.x}px ${n.y}px ${n.blur}px ${n.color})`).join(" ");return[`color:${e}`,`background-image:${r.map(_).join(", ")}`,"background-size:cover","-webkit-background-clip:text","background-clip:text","-webkit-text-fill-color:transparent",s?`filter:${s}`:""].filter(Boolean).join(";")}class g{pixl;paints=new Map;queue=new Map;timer=null;constructor(t){this.pixl=t}css=j;emoteUrl(t,e=1){return T(t,e)}badgesHtml(t){return M(t)}paintCss(t,e){return w(t,e)}html(t,e,r){return f(t,e,r)}async emotes(){return(await this.pixl.request("/emotes")).emotes}paint(t){let e=this.paints.get(t);if(!e)e=new Promise((r)=>{this.queue.set(t,r),this.timer??=setTimeout(()=>this.flush(),20)}),this.paints.set(t,e);return e}async paintStyle(t,e="inherit"){let r=t?await this.paint(t):null;return r?w(r,e):`color:${e}`}async flush(){this.timer=null;let t=this.queue;this.queue=new Map;let e=[...t.keys()],r=new Map;try{for(let s=0;s<e.length;s+=100){let n=await this.pixl.request("/7tv/paints",{ids:e.slice(s,s+100).join(",")});for(let i of n.paints)r.set(i.id,i)}}catch(s){console.warn("[pixl] 7TV-Paints nicht geladen:",s);for(let n of e)this.paints.delete(n)}for(let[s,n]of t)n(r.get(s)??null)}}var S=[2000,5000,1e4,30000];class v{ready=!1;error=null;pixl;since;types;handler;delivered=new Set;errorListeners=new Set;buffer=null;loadSeq=0;stopped=!1;latestAt=null;retryTimer=null;retryAttempt=0;unsubscribeStatus;onEvent=(t,e)=>this.handleLive(e);constructor(t,e,r){if(e?.since===void 0)throw Error("pixl.feed: `since` fehlt");this.pixl=t,this.since=e.since instanceof Date?e.since.toISOString():String(e.since),this.types=e.types?.length?new Set(e.types):null,this.handler=r,t.on("*",this.onEvent),this.unsubscribeStatus=t.onStatus((s)=>{if(s==="connected")this.retryAttempt=0,this.load()})}onError(t){if(this.errorListeners.add(t),this.error)t(this.error);return()=>this.errorListeners.delete(t)}stop(){this.stopped=!0,this.clearRetry(),this.pixl.off("*",this.onEvent),this.unsubscribeStatus(),this.errorListeners.clear(),this.buffer=null}async load(){let t=++this.loadSeq;this.clearRetry(),this.buffer=[];let e=null;try{let s=await this.fetchHistory();if(t!==this.loadSeq||this.stopped)return;this.error=null,this.retryAttempt=0;for(let n of s)this.deliver({event:n.event,data:n.data,replay:!!(n.data?.replay||n.data?.test),id:n.id,at:n.at,history:!0})}catch(s){if(t!==this.loadSeq||this.stopped)return;e=s instanceof Error?s:Error(String(s)),this.error=e;for(let n of this.errorListeners)n(e)}let r=this.buffer??[];this.buffer=null,this.ready=!0;for(let s of r)this.deliver(s);if(e)this.scheduleRetry(e)}async fetchHistory(){let t=this.latestAt===null?null:this.latestAt-300000,e=[],r;do{let s=await this.pixl.events({since:this.since,types:this.types?[...this.types]:void 0,limit:200,cursor:r});e.push(...s.events),r=s.nextCursor??void 0;let n=s.events[s.events.length-1];if(t!==null&&n&&Date.parse(n.at)<t)break}while(r);return e.reverse()}scheduleRetry(t){let e=t.status;if(e!==void 0&&e>=400&&e<500&&e!==429&&e!==408)return;if(this.pixl.status!=="connected")return;let r=S[Math.min(this.retryAttempt,S.length-1)];this.retryAttempt+=1,this.retryTimer=setTimeout(()=>{if(this.retryTimer=null,!this.stopped&&this.pixl.status==="connected")this.load()},r+Math.random()*r*0.3)}clearRetry(){if(this.retryTimer)clearTimeout(this.retryTimer);this.retryTimer=null}handleLive(t){if(this.stopped)return;if(this.types&&!this.types.has(t.event))return;if(this.buffer){this.buffer.push(t);return}this.deliver(t)}deliver(t){if(t.id){if(this.delivered.has(t.id))return;this.delivered.add(t.id)}if(t.at){let e=Date.parse(t.at);if(!Number.isNaN(e)&&(this.latestAt===null||e>this.latestAt))this.latestAt=e}try{this.handler(t.data,t)}catch(e){console.error("[pixl] Fehler im feed()-Handler:",e)}}}class x{pixl;constructor(t){this.pixl=t}async speak(t,e={}){return await(await this.pixl._fetch("/tts",{method:"POST",json:{text:t,voice:e.voice,filters:e.filters}})).json()}async voices(){return await(await this.pixl._fetch("/tts/voices")).json()}async audio(t){return(await this.pixl._fetch(`/tts/${encodeURIComponent(t)}/audio`)).blob()}async marks(t){return(await(await this.pixl._fetch(`/tts/${encodeURIComponent(t)}/marks`)).json()).marks}player(t={}){return new k(this.pixl,this,t)}}class k{pixl;tts;options;queue=[];current=null;wordFrame=null;volume;destroyed=!1;warnedAutoplay=!1;onReady;onSkip;onClear;constructor(t,e,r){this.pixl=t;this.tts=e;this.options=r;this.volume=P(r.volume??1),this.onReady=(s,n)=>{let i=I(s,n);if(!i||n.replay&&this.options.ignoreReplays)return;if(this.current?.item.id===i.id||this.queue.some((a)=>a.id===i.id))return;this.queue.push(i);let o=this.options.maxQueue??20;if(this.queue.length>o)this.queue.splice(0,this.queue.length-o);this.next()},this.onSkip=(s)=>{let n=typeof s.id==="string"?s.id:void 0;if(!n||this.current?.item.id===n)this.skip();else this.queue=this.queue.filter((i)=>i.id!==n)},this.onClear=()=>this.clear(),t.on("tts.ready",this.onReady),t.on("tts.skip",this.onSkip),t.on("tts.clear",this.onClear)}skip(){if(!this.current)return;this.current.skipped=!0,this.current.finish()}clear(){this.queue=[],this.skip()}setVolume(t){if(this.volume=P(t),this.current?.audio)this.current.audio.volume=this.volume}get pending(){return this.queue}destroy(){this.destroyed=!0,this.pixl.off("tts.ready",this.onReady),this.pixl.off("tts.skip",this.onSkip),this.pixl.off("tts.clear",this.onClear),this.clear()}followWords(t,e,r){this.stopWords();let s=0,n=(o)=>r[o].end??r[o+1]?.start??(Number.isFinite(t.duration)?Math.round(t.duration*1000):r[o].start),i=()=>{let o=t.currentTime*1000;while(s<r.length&&r[s].start<=o){let a=s++,l={word:r[a].word,index:a,start:r[a].start,end:n(a)};y(()=>this.options.onWord?.(l,e))}this.wordFrame=s<r.length&&!t.ended?E(i):null};this.wordFrame=E(i)}stopWords(){if(this.wordFrame!==null)L(this.wordFrame);this.wordFrame=null}async next(){if(this.current||this.destroyed)return;let t=this.queue.shift();if(!t)return;let e=!1,r,s=new Promise((i)=>r=i),n={item:t,audio:null,url:null,skipped:!1,finish:()=>{if(e)return;if(e=!0,this.stopWords(),n.audio?.pause(),n.url)URL.revokeObjectURL(n.url);r()}};this.current=n;try{let[i,o]=await Promise.all([this.tts.audio(t.id),this.options.onWord?this.tts.marks(t.id).catch((a)=>{console.warn("[pixl] Zeitmarken nicht verfügbar, onWord bleibt für diesen Eintrag stumm:",a);return}):Promise.resolve(void 0)]);if(o)t.marks=o;if(!e){n.url=URL.createObjectURL(i);let a=new Audio(n.url);if(a.volume=this.volume,a.addEventListener("ended",n.finish),a.addEventListener("error",n.finish),n.audio=a,y(()=>this.options.onStart?.(t)),o?.length)this.followWords(a,t,o);await a.play().catch((l)=>{if(!this.warnedAutoplay&&l instanceof DOMException&&l.name==="NotAllowedError")this.warnedAutoplay=!0,console.warn("[pixl] Der Browser blockiert die Wiedergabe ohne Klick. In OBS und StreamElements passiert das nicht, im normalen Browser einmal auf die Seite klicken.");n.finish()})}}catch(i){console.error("[pixl] TTS-Audio konnte nicht geladen werden:",i),n.finish()}if(await s,y(()=>this.options.onEnd?.(t,{skipped:n.skipped})),this.current=null,this.queue.length&&!this.destroyed)setTimeout(()=>void this.next(),this.options.gapMs??500)}}function E(t){return typeof requestAnimationFrame==="function"?requestAnimationFrame(t):setTimeout(t,30)}function L(t){if(typeof cancelAnimationFrame==="function")cancelAnimationFrame(t);else clearTimeout(t)}function I(t,e){if(typeof t.id!=="string")return null;return{id:t.id,text:typeof t.text==="string"?t.text:"",voice:typeof t.voice==="string"?t.voice:"",source:typeof t.source==="string"?t.source:"",...typeof t.eventId==="string"?{eventId:t.eventId}:{},...t.textTruncated===!0?{textTruncated:!0}:{},replay:e.replay}}function P(t){return Number.isFinite(t)?Math.min(1,Math.max(0,t)):1}function y(t){try{t()}catch(e){console.error("[pixl] Fehler in einem TTS-Handler:",e)}}class h extends Error{status;problem;constructor(t,e,r){super(e);this.name="PixlApiError",this.status=t,this.problem=r}}var q="https://pixl.lennox.zip",C=[500,1000,2000,3000,5000,1e4],F=45000,U=15000;function R(t){if(t instanceof Date)return t.toISOString();return String(t)}class p{static defaultBaseUrl;baseUrl;key;tts;chat;handlers=new Map;stateHandlers=new Map;statusHandlers=new Set;states=new Map;_status="closed";abort=null;reconnectTimer=null;watchdog=null;attempt=0;stopped=!1;logs;everConnected=!1;lostAt=null;constructor(t){if(!t?.key)throw Error("pixl: `key` fehlt (API-Schlüssel aus den Einstellungen)");if(this.key=t.key,this.logs=t.logs!==!1,this.baseUrl=(t.baseUrl??p.defaultBaseUrl??q).replace(/\/+$/,""),this.tts=new x(this),this.chat=new g(this),t.autoConnect!==!1)queueMicrotask(()=>this.connect())}on(t,e){let r=this.handlers.get(t);if(!r)this.handlers.set(t,r=new Set);return r.add(e),this}off(t,e){return this.handlers.get(t)?.delete(e),this}state(t,e){let r=this.stateHandlers.get(t);if(!r)this.stateHandlers.set(t,r=new Set);if(r.add(e),this.states.has(t))e(this.states.get(t),t);return this}offState(t,e){return this.stateHandlers.get(t)?.delete(e),this}getState(t){return this.states.get(t)}get status(){return this._status}onStatus(t){return this.statusHandlers.add(t),t(this._status),()=>this.statusHandlers.delete(t)}setStatus(t){if(t===this._status)return;this._status=t;for(let e of this.statusHandlers)m(()=>e(t))}async request(t,e={}){let r=new URL(`${this.baseUrl}/api/v1${t}`);for(let[n,i]of Object.entries(e))if(i!==void 0&&i!=="")r.searchParams.set(n,i);let s=await fetch(r,{headers:{Authorization:`Bearer ${this.key}`}});if(!s.ok){let n=await s.json().catch(()=>null);throw new h(s.status,n?.error??`HTTP ${s.status}`)}return await s.json()}async _fetch(t,e={}){let r={Authorization:`Bearer ${this.key}`};if(e.json!==void 0)r["Content-Type"]="application/json";let s=await fetch(`${this.baseUrl}/api/v1${t}`,{method:e.method??"GET",headers:r,body:e.json!==void 0?JSON.stringify(e.json):void 0});if(!s.ok){let n=await s.json().catch(()=>null);throw new h(s.status,n?.error??`HTTP ${s.status}`,n?.problem)}return s}events(t={}){return this.request("/events",{since:t.since!==void 0?R(t.since):void 0,until:t.until!==void 0?R(t.until):void 0,types:t.types?.join(","),limit:t.limit!==void 0?String(t.limit):void 0,cursor:t.cursor})}org(){return this.request("/org")}subs(){return this.request("/subs")}nowPlaying(){return this.request("/spotify/now-playing")}static progress(t,e=Date.now()){if(!t.track)return 0;let r=t.playing?t.progressMs+(e-Date.parse(t.at)):t.progressMs;return Math.max(0,Math.min(r,t.track.durationMs))}streamElements(t,e={}){let r={};for(let[s,n]of Object.entries(e))r[s]=n===void 0?void 0:String(n);return this.request(`/streamelements/${t}`,r)}async fetchStates(){return(await this.request("/states")).states}async fetchAllEvents(t={}){let e=[],r;do{let s=await this.events({...t,limit:200,cursor:r});e.push(...s.events),r=s.nextCursor??void 0}while(r);return e.reverse()}feed(t,e){return new v(this,t,e)}connect(){if(this.abort)return;this.stopped=!1,this.run()}disconnect(){if(this.stopped=!0,this.reconnectTimer)clearTimeout(this.reconnectTimer);this.reconnectTimer=null,this.abort?.abort(),this.abort=null,this.setStatus("closed")}async run(){let t=new AbortController;this.abort=t,this.setStatus("connecting");let e=!1;try{this.watchdog=setTimeout(()=>t.abort(),U);let r=await fetch(`${this.baseUrl}/api/v1/stream`,{headers:{Authorization:`Bearer ${this.key}`,Accept:"text/event-stream"},signal:t.signal,cache:"no-store"});if(this.clearWatchdog(),r.status===401||r.status===403){e=!0;return}if(!r.ok||!r.body)throw Error(`HTTP ${r.status}`);if(this.attempt=0,this.lostAt!==null)this.log(`Live-Verbindung wieder da nach ${((Date.now()-this.lostAt)/1000).toFixed(1)} s`);this.lostAt=null,this.everConnected=!0,this.setStatus("connected"),await this.readStream(r.body,t,()=>{e=!0})}catch{}finally{if(this.clearWatchdog(),this.abort===t)this.abort=null;if(e)this.log("Live-Verbindung abgelehnt (Schlüssel ungültig oder widerrufen), kein neuer Versuch"),this.stopped=!0,this.setStatus("unauthorized");else if(!this.stopped){if(this.lostAt===null)this.lostAt=Date.now(),this.log(this.everConnected?"Live-Verbindung getrennt, verbinde neu":"Live-Verbindung nicht erreichbar, versuche es weiter");this.setStatus("disconnected"),this.scheduleReconnect()}}}log(t){if(this.logs)console.info(`[pixl] ${t}`)}scheduleReconnect(){let t=C[Math.min(this.attempt,C.length-1)];this.attempt+=1;let e=t+Math.random()*t*0.3;this.reconnectTimer=setTimeout(()=>{if(this.reconnectTimer=null,!this.stopped)this.connect()},e)}armWatchdog(t){this.clearWatchdog(),this.watchdog=setTimeout(()=>t.abort(),F)}clearWatchdog(){if(this.watchdog)clearTimeout(this.watchdog);this.watchdog=null}async readStream(t,e,r){let s=t.getReader(),n=new TextDecoder,i="",o=[];this.armWatchdog(e);for(;;){let{value:a,done:l}=await s.read();if(l)return;this.armWatchdog(e),i+=n.decode(a,{stream:!0});let u;while((u=i.indexOf(`
|
|
5
|
-
`))>=0){let c=i.slice(0,u).replace(/\r$/,"");if(i=i.slice(u+1),c===""){if(o.length){let
|
|
6
|
-
`);if(o=[],this.dispatch(
|
|
4
|
+
.pixl-badge{height:1.1em;vertical-align:middle;margin-right:.25em}`;function L(t){switch(t.type){case"linear-gradient":case"radial-gradient":{let e=t.stops.map((n)=>`${n.color} ${Math.round(n.at*1000)/10}%`).join(", "),r=`${t.repeating?"repeating-":""}${t.type}`;return t.type==="linear-gradient"?`${r}(${t.angle}deg, ${e})`:`${r}(${t.shape}, ${e})`}case"color":return`linear-gradient(${t.color}, ${t.color})`;case"image":return`url("${t.url.replace(/"/g,"%22")}")`}}function w(t,e="inherit"){let r=t.layers.filter((s)=>s.opacity>0);if(!r.length)return`color:${e}`;let n=t.shadows.map((s)=>`drop-shadow(${s.x}px ${s.y}px ${s.blur}px ${s.color})`).join(" ");return[`color:${e}`,`background-image:${r.map(L).join(", ")}`,"background-size:cover","-webkit-background-clip:text","background-clip:text","-webkit-text-fill-color:transparent",n?`filter:${n}`:""].filter(Boolean).join(";")}class g{pixl;paints=new Map;queue=new Map;timer=null;constructor(t){this.pixl=t}css=_;emoteUrl(t,e=1){return S(t,e)}badgesHtml(t){return j(t)}paintCss(t,e){return w(t,e)}html(t,e,r){return f(t,e,r)}async emotes(){return(await this.pixl.request("/emotes")).emotes}paint(t){let e=this.paints.get(t);if(!e)e=new Promise((r)=>{this.queue.set(t,r),this.timer??=setTimeout(()=>this.flush(),20)}),this.paints.set(t,e);return e}async paintStyle(t,e="inherit"){let r=t?await this.paint(t):null;return r?w(r,e):`color:${e}`}async flush(){this.timer=null;let t=this.queue;this.queue=new Map;let e=[...t.keys()],r=new Map;try{for(let n=0;n<e.length;n+=100){let s=await this.pixl.request("/7tv/paints",{ids:e.slice(n,n+100).join(",")});for(let i of s.paints)r.set(i.id,i)}}catch(n){console.warn("[pixl] 7TV-Paints nicht geladen:",n);for(let s of e)this.paints.delete(s)}for(let[n,s]of t)s(r.get(n)??null)}}var T=[2000,5000,1e4,30000];class v{ready=!1;error=null;pixl;since;types;handler;delivered=new Set;errorListeners=new Set;buffer=null;loadSeq=0;stopped=!1;latestAt=null;retryTimer=null;retryAttempt=0;unsubscribeStatus;onEvent=(t,e)=>this.handleLive(e);constructor(t,e,r){if(e?.since===void 0)throw Error("pixl.feed: `since` fehlt");this.pixl=t,this.since=e.since instanceof Date?e.since.toISOString():String(e.since),this.types=e.types?.length?new Set(e.types):null,this.handler=r,t.on("*",this.onEvent),this.unsubscribeStatus=t.onStatus((n)=>{if(n==="connected")this.retryAttempt=0,this.load()})}onError(t){if(this.errorListeners.add(t),this.error)t(this.error);return()=>this.errorListeners.delete(t)}stop(){this.stopped=!0,this.clearRetry(),this.pixl.off("*",this.onEvent),this.unsubscribeStatus(),this.errorListeners.clear(),this.buffer=null}async load(){let t=++this.loadSeq;this.clearRetry(),this.buffer=[];let e=null;try{let n=await this.fetchHistory();if(t!==this.loadSeq||this.stopped)return;this.error=null,this.retryAttempt=0;for(let s of n)this.deliver({event:s.event,data:s.data,replay:!!(s.data?.replay||s.data?.test),id:s.id,at:s.at,history:!0})}catch(n){if(t!==this.loadSeq||this.stopped)return;e=n instanceof Error?n:Error(String(n)),this.error=e;for(let s of this.errorListeners)s(e)}let r=this.buffer??[];this.buffer=null,this.ready=!0;for(let n of r)this.deliver(n);if(e)this.scheduleRetry(e)}async fetchHistory(){let t=this.latestAt===null?null:this.latestAt-300000,e=[],r;do{let n=await this.pixl.events({since:this.since,types:this.types?[...this.types]:void 0,limit:200,cursor:r});e.push(...n.events),r=n.nextCursor??void 0;let s=n.events[n.events.length-1];if(t!==null&&s&&Date.parse(s.at)<t)break}while(r);return e.reverse()}scheduleRetry(t){let e=t.status;if(e!==void 0&&e>=400&&e<500&&e!==429&&e!==408)return;if(this.pixl.status!=="connected")return;let r=T[Math.min(this.retryAttempt,T.length-1)];this.retryAttempt+=1,this.retryTimer=setTimeout(()=>{if(this.retryTimer=null,!this.stopped&&this.pixl.status==="connected")this.load()},r+Math.random()*r*0.3)}clearRetry(){if(this.retryTimer)clearTimeout(this.retryTimer);this.retryTimer=null}handleLive(t){if(this.stopped)return;if(this.types&&!this.types.has(t.event))return;if(this.buffer){this.buffer.push(t);return}this.deliver(t)}deliver(t){if(t.id){if(this.delivered.has(t.id))return;this.delivered.add(t.id)}if(t.at){let e=Date.parse(t.at);if(!Number.isNaN(e)&&(this.latestAt===null||e>this.latestAt))this.latestAt=e}try{this.handler(t.data,t)}catch(e){console.error("[pixl] Fehler im feed()-Handler:",e)}}}class x{pixl;constructor(t){this.pixl=t}async speak(t,e={}){return await(await this.pixl._fetch("/tts",{method:"POST",json:{text:t,voice:e.voice,filters:e.filters}})).json()}async voices(){return await(await this.pixl._fetch("/tts/voices")).json()}async audio(t){return(await this.pixl._fetch(`/tts/${encodeURIComponent(t)}/audio`)).blob()}async marks(t){return(await(await this.pixl._fetch(`/tts/${encodeURIComponent(t)}/marks`)).json()).marks}player(t={}){return new k(this.pixl,this,t)}}class k{pixl;tts;options;queue=[];current=null;wordFrame=null;volume;destroyed=!1;warnedAutoplay=!1;onReady;onSkip;onClear;constructor(t,e,r){this.pixl=t;this.tts=e;this.options=r;this.volume=P(r.volume??1),this.onReady=(n,s)=>{let i=F(n,s);if(!i||s.replay&&this.options.ignoreReplays)return;if(this.current?.item.id===i.id||this.queue.some((a)=>a.id===i.id))return;this.queue.push(i);let o=this.options.maxQueue??20;if(this.queue.length>o)this.queue.splice(0,this.queue.length-o);this.next()},this.onSkip=(n)=>{let s=typeof n.id==="string"?n.id:void 0;if(!s||this.current?.item.id===s)this.skip();else this.queue=this.queue.filter((i)=>i.id!==s)},this.onClear=()=>this.clear(),t.on("tts.ready",this.onReady),t.on("tts.skip",this.onSkip),t.on("tts.clear",this.onClear)}skip(){if(!this.current)return;this.current.skipped=!0,this.current.finish()}clear(){this.queue=[],this.skip()}setVolume(t){if(this.volume=P(t),this.current?.audio)this.current.audio.volume=this.volume}get pending(){return this.queue}destroy(){this.destroyed=!0,this.pixl.off("tts.ready",this.onReady),this.pixl.off("tts.skip",this.onSkip),this.pixl.off("tts.clear",this.onClear),this.clear()}followWords(t,e,r){this.stopWords();let n=0,s=(o)=>r[o].end??r[o+1]?.start??(Number.isFinite(t.duration)?Math.round(t.duration*1000):r[o].start),i=()=>{let o=t.currentTime*1000;while(n<r.length&&r[n].start<=o){let a=n++,l={word:r[a].word,index:a,start:r[a].start,end:s(a)};y(()=>this.options.onWord?.(l,e))}this.wordFrame=n<r.length&&!t.ended?E(i):null};this.wordFrame=E(i)}stopWords(){if(this.wordFrame!==null)I(this.wordFrame);this.wordFrame=null}async next(){if(this.current||this.destroyed)return;let t=this.queue.shift();if(!t)return;let e=!1,r,n=new Promise((i)=>r=i),s={item:t,audio:null,url:null,skipped:!1,finish:()=>{if(e)return;if(e=!0,this.stopWords(),s.audio?.pause(),s.url)URL.revokeObjectURL(s.url);r()}};this.current=s;try{let[i,o]=await Promise.all([this.tts.audio(t.id),this.options.onWord?this.tts.marks(t.id).catch((a)=>{console.warn("[pixl] Zeitmarken nicht verfügbar, onWord bleibt für diesen Eintrag stumm:",a);return}):Promise.resolve(void 0)]);if(o)t.marks=o;if(!e){s.url=URL.createObjectURL(i);let a=new Audio(s.url);if(a.volume=this.volume,a.addEventListener("ended",s.finish),a.addEventListener("error",s.finish),s.audio=a,y(()=>this.options.onStart?.(t)),o?.length)this.followWords(a,t,o);await a.play().catch((l)=>{if(!this.warnedAutoplay&&l instanceof DOMException&&l.name==="NotAllowedError")this.warnedAutoplay=!0,console.warn("[pixl] Der Browser blockiert die Wiedergabe ohne Klick. In OBS und StreamElements passiert das nicht, im normalen Browser einmal auf die Seite klicken.");s.finish()})}}catch(i){console.error("[pixl] TTS-Audio konnte nicht geladen werden:",i),s.finish()}if(await n,y(()=>this.options.onEnd?.(t,{skipped:s.skipped})),this.current=null,this.queue.length&&!this.destroyed)setTimeout(()=>void this.next(),this.options.gapMs??500)}}function E(t){return typeof requestAnimationFrame==="function"?requestAnimationFrame(t):setTimeout(t,30)}function I(t){if(typeof cancelAnimationFrame==="function")cancelAnimationFrame(t);else clearTimeout(t)}function F(t,e){if(typeof t.id!=="string")return null;return{id:t.id,text:typeof t.text==="string"?t.text:"",voice:typeof t.voice==="string"?t.voice:"",source:typeof t.source==="string"?t.source:"",...typeof t.eventId==="string"?{eventId:t.eventId}:{},...t.textTruncated===!0?{textTruncated:!0}:{},replay:e.replay}}function P(t){return Number.isFinite(t)?Math.min(1,Math.max(0,t)):1}function y(t){try{t()}catch(e){console.error("[pixl] Fehler in einem TTS-Handler:",e)}}class h extends Error{status;problem;constructor(t,e,r){super(e);this.name="PixlApiError",this.status=t,this.problem=r}}var q="https://pixl.lennox.zip",C=[500,1000,2000,3000,5000,1e4],U=45000,W=15000;function R(t){if(t instanceof Date)return t.toISOString();return String(t)}class p{static defaultBaseUrl;baseUrl;key;tts;chat;handlers=new Map;stateHandlers=new Map;statusHandlers=new Set;states=new Map;_status="closed";abort=null;reconnectTimer=null;watchdog=null;attempt=0;stopped=!1;logs;everConnected=!1;lostAt=null;constructor(t){if(!t?.key)throw Error("pixl: `key` fehlt (API-Schlüssel aus den Einstellungen)");if(this.key=t.key,this.logs=t.logs!==!1,this.baseUrl=(t.baseUrl??p.defaultBaseUrl??q).replace(/\/+$/,""),this.tts=new x(this),this.chat=new g(this),t.autoConnect!==!1)queueMicrotask(()=>this.connect())}on(t,e){let r=this.handlers.get(t);if(!r)this.handlers.set(t,r=new Set);return r.add(e),this}off(t,e){return this.handlers.get(t)?.delete(e),this}state(t,e){let r=this.stateHandlers.get(t);if(!r)this.stateHandlers.set(t,r=new Set);if(r.add(e),this.states.has(t))e(this.states.get(t),t);return this}offState(t,e){return this.stateHandlers.get(t)?.delete(e),this}getState(t){return this.states.get(t)}get status(){return this._status}onStatus(t){return this.statusHandlers.add(t),t(this._status),()=>this.statusHandlers.delete(t)}setStatus(t){if(t===this._status)return;this._status=t;for(let e of this.statusHandlers)m(()=>e(t))}async request(t,e={}){let r=new URL(`${this.baseUrl}/api/v1${t}`);for(let[s,i]of Object.entries(e))if(i!==void 0&&i!=="")r.searchParams.set(s,i);let n=await fetch(r,{headers:{Authorization:`Bearer ${this.key}`}});if(!n.ok){let s=await n.json().catch(()=>null);throw new h(n.status,s?.error??`HTTP ${n.status}`)}return await n.json()}async _fetch(t,e={}){let r={Authorization:`Bearer ${this.key}`};if(e.json!==void 0)r["Content-Type"]="application/json";let n=await fetch(`${this.baseUrl}/api/v1${t}`,{method:e.method??"GET",headers:r,body:e.json!==void 0?JSON.stringify(e.json):void 0});if(!n.ok){let s=await n.json().catch(()=>null);throw new h(n.status,s?.error??`HTTP ${n.status}`,s?.problem)}return n}events(t={}){return this.request("/events",{since:t.since!==void 0?R(t.since):void 0,until:t.until!==void 0?R(t.until):void 0,types:t.types?.join(","),limit:t.limit!==void 0?String(t.limit):void 0,cursor:t.cursor})}org(){return this.request("/org")}subs(){return this.request("/subs")}nowPlaying(){return this.request("/spotify/now-playing")}static progress(t,e=Date.now()){if(!t.track)return 0;let r=t.playing?t.progressMs+(e-Date.parse(t.at)):t.progressMs;return Math.max(0,Math.min(r,t.track.durationMs))}streamElements(t,e={}){let r={};for(let[n,s]of Object.entries(e))r[n]=s===void 0?void 0:String(s);return this.request(`/streamelements/${t}`,r)}async fetchStates(){return(await this.request("/states")).states}async fetchAllEvents(t={}){let e=[],r;do{let n=await this.events({...t,limit:200,cursor:r});e.push(...n.events),r=n.nextCursor??void 0}while(r);return e.reverse()}feed(t,e){return new v(this,t,e)}connect(){if(this.abort)return;this.stopped=!1,this.run()}disconnect(){if(this.stopped=!0,this.reconnectTimer)clearTimeout(this.reconnectTimer);this.reconnectTimer=null,this.abort?.abort(),this.abort=null,this.setStatus("closed")}async run(){let t=new AbortController;this.abort=t,this.setStatus("connecting");let e=!1;try{this.watchdog=setTimeout(()=>t.abort(),W);let r=await fetch(`${this.baseUrl}/api/v1/stream`,{headers:{Authorization:`Bearer ${this.key}`,Accept:"text/event-stream"},signal:t.signal,cache:"no-store"});if(this.clearWatchdog(),r.status===401||r.status===403){e=!0;return}if(!r.ok||!r.body)throw Error(`HTTP ${r.status}`);if(this.attempt=0,this.lostAt!==null)this.log(`Live-Verbindung wieder da nach ${((Date.now()-this.lostAt)/1000).toFixed(1)} s`);this.lostAt=null,this.everConnected=!0,this.setStatus("connected"),await this.readStream(r.body,t,()=>{e=!0})}catch{}finally{if(this.clearWatchdog(),this.abort===t)this.abort=null;if(e)this.log("Live-Verbindung abgelehnt (Schlüssel ungültig oder widerrufen), kein neuer Versuch"),this.stopped=!0,this.setStatus("unauthorized");else if(!this.stopped){if(this.lostAt===null)this.lostAt=Date.now(),this.log(this.everConnected?"Live-Verbindung getrennt, verbinde neu":"Live-Verbindung nicht erreichbar, versuche es weiter");this.setStatus("disconnected"),this.scheduleReconnect()}}}log(t){if(this.logs)console.info(`[pixl] ${t}`)}scheduleReconnect(){let t=C[Math.min(this.attempt,C.length-1)];this.attempt+=1;let e=t+Math.random()*t*0.3;this.reconnectTimer=setTimeout(()=>{if(this.reconnectTimer=null,!this.stopped)this.connect()},e)}armWatchdog(t){this.clearWatchdog(),this.watchdog=setTimeout(()=>t.abort(),U)}clearWatchdog(){if(this.watchdog)clearTimeout(this.watchdog);this.watchdog=null}async readStream(t,e,r){let n=t.getReader(),s=new TextDecoder,i="",o=[];this.armWatchdog(e);for(;;){let{value:a,done:l}=await n.read();if(l)return;this.armWatchdog(e),i+=s.decode(a,{stream:!0});let u;while((u=i.indexOf(`
|
|
5
|
+
`))>=0){let c=i.slice(0,u).replace(/\r$/,"");if(i=i.slice(u+1),c===""){if(o.length){let O=o.join(`
|
|
6
|
+
`);if(o=[],this.dispatch(O)==="revoked"){r(),await n.cancel().catch(()=>{});return}}}else if(c.startsWith("data:"))o.push(c.slice(5).replace(/^ /,""))}}}dispatch(t){let e;try{e=JSON.parse(t)}catch{return}if(e.event==="api_key.revoked")return"revoked";if(e.event==="state.snapshot"){let n=e.data?.states??[];for(let{key:s,value:i}of n)this.setState(s,i);return}if(e.event==="state.updated"){let{key:n,value:s}=e.data;if(typeof n==="string")this.setState(n,s);return}let r={event:e.event,data:e.data??{},replay:!!e.replay,id:e.id,at:e.at};this.emit(r)}emit(t){if(t.event==="twitch.chat_message"&&typeof t.data.text==="string"&&t.data.html===void 0)t.data.html=f(t.data.text,t.data.emotes);for(let e of this.handlers.get(t.event)??[])m(()=>e(t.data,t));for(let e of this.handlers.get("*")??[])m(()=>e(t.data,t))}setState(t,e){this.states.set(t,e);for(let r of this.stateHandlers.get(t)??[])m(()=>r(e,t))}}function m(t){try{t()}catch(e){console.error("[pixl] Fehler in einem Handler:",e)}}function A(t){if(!t)return;try{let e=new URL(t);return e.pathname.replace(/\/+$/,"").endsWith("/sdk/pixl.js")?e.origin:void 0}catch{return}}var D=typeof document<"u"?document.currentScript:null,H=A(D?.src);if(H)p.defaultBaseUrl=H;Object.assign(globalThis,{Pixl:p,PixlApiError:h});})();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pixl-app/sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "pixl SDK: Events, Chat, Spotify und TTS eines Kanals in eigenen Widgets, etwa StreamElements Custom Widgets oder OBS-Browserquellen",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://docs.pixl.lennox.zip/sdk",
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"access": "public"
|
|
38
38
|
},
|
|
39
39
|
"scripts": {
|
|
40
|
+
"dev": "bun scripts/dev.ts",
|
|
40
41
|
"build": "bun run build:script && bun run build:esm && bun run build:types",
|
|
41
42
|
"build:script": "bun build ./src/global.ts --outfile ./dist/pixl.js --target browser --format iife --minify",
|
|
42
43
|
"build:esm": "bun build ./src/index.ts --outfile ./dist/index.js --target browser --format esm",
|
|
@@ -44,7 +45,8 @@
|
|
|
44
45
|
"check": "tsc -p tsconfig.json",
|
|
45
46
|
"format": "prettier --write .",
|
|
46
47
|
"lint": "prettier --check .",
|
|
47
|
-
"check:version": "bun scripts/check-version.ts"
|
|
48
|
+
"check:version": "bun scripts/check-version.ts",
|
|
49
|
+
"test": "bun test"
|
|
48
50
|
},
|
|
49
51
|
"devDependencies": {
|
|
50
52
|
"@types/obs-studio": "*",
|