import { Injectable, EventEmitter } from '@angular/core'; import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Observable, Subscription } from 'rxjs'; import { finalize, shareReplay } from 'rxjs/operators'; import { PluginDescription, State, Slide, ServiceItem, Theme, MainView, SystemInformation, Credentials, AuthToken } from './responses'; import { environment } from '../environments/environment'; const deserialize = (json, cls) => { const inst = new cls(); for (const p in json) { if (!json.hasOwnProperty(p)) { continue; } inst[p] = json[p]; } return inst; }; const httpOptions = { headers: new HttpHeaders({ 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }) }; const WEBSOCKET_RECONNECT_TIMEOUT = 5 * 1000; export enum WebSocketStatus { Open, Closed } @Injectable() export class OpenLPService { private apiURL: string; private host: string; public stateChanged$: EventEmitter; public webSocketStateChanged$: EventEmitter; private isTwelveHourTime = true; private webSocketTimeoutHandle: any = 0; private ws: WebSocket = null; private retrieveSystemInformationSubscription: Subscription; constructor(private http: HttpClient) { const host = window.location.hostname; let port: string; if (environment.production) { port = window.location.port; } else { port = '4316'; } this.apiURL = `http://${host}:${port}/api/v2`; this.host = host; this.stateChanged$ = new EventEmitter(); this.webSocketStateChanged$ = new EventEmitter(); this.createWebSocket(); } setAuthToken(token: string): void { httpOptions.headers = httpOptions.headers.set('Authorization', token); } getIsTwelveHourTime(): boolean { return this.isTwelveHourTime; } retrieveSystemInformation(): Observable { return this.doGet(`${this.apiURL}/core/system`); } getMainImage(): Observable { return this.doGet(`${this.apiURL}/core/live-image`); } getSearchablePlugins(): Observable { return this.doGet(`${this.apiURL}/core/plugins`); } search(plugin, text): Observable { return this.doGet(`${this.apiURL}/plugins/${plugin}/search?text=${text}`); } getSearchOptions(plugin): Observable { return this.doGet(`${this.apiURL}/plugins/${plugin}/search-options`); } setSearchOption(plugin, option, value): Observable { return this.doPost(`${this.apiURL}/plugins/${plugin}/search-options`, {option, value}); } getServiceItems(): Observable { return this.doGet(`${this.apiURL}/service/items`); } setServiceItem(id: any): Observable { return this.doPost(`${this.apiURL}/service/show`, {id}); } nextItem(): Observable { return this.doPost(`${this.apiURL}/service/progress`, {action: 'next'}); } previousItem(): Observable { return this.doPost(`${this.apiURL}/service/progress`, {action: 'previous'}); } getServiceItem(): Observable { return this.doGet(`${this.apiURL}/controller/live-items`); } getNotes(): Observable { return this.doGet(`${this.apiURL}/controller/notes`); } setSlide(id: any): Observable { return this.doPost(`${this.apiURL}/controller/show`, {id}); } nextSlide(): Observable { return this.doPost(`${this.apiURL}/controller/progress`, {action: 'next'}); } previousSlide(): Observable { return this.doPost(`${this.apiURL}/controller/progress`, {action: 'previous'}); } getThemeLevel(): Observable { return this.doGet(`${this.apiURL}/controller/theme-level`); } getThemes(): Observable { return this.doGet(`${this.apiURL}/controller/themes`); } setThemeLevel(level): Observable { return this.doPost(`${this.apiURL}/controller/theme-level`, {level}); } getTheme(): Observable { return this.doGet(`${this.apiURL}/controller/theme`); } setTheme(theme: string): Observable { return this.doPost(`${this.apiURL}/controller/theme`, {theme}); } blankDisplay(): Observable { return this.doPost(`${this.apiURL}/core/display`, {display: 'blank'}); } themeDisplay(): Observable { return this.doPost(`${this.apiURL}/core/display`, {display: 'theme'}); } desktopDisplay(): Observable { return this.doPost(`${this.apiURL}/core/display`, {display: 'desktop'}); } showDisplay(): Observable { return this.doPost(`${this.apiURL}/core/display`, {display: 'show'}); } showAlert(text): Observable { return this.doPost(`${this.apiURL}/plugins/alerts`, {text}); } sendItemLive(plugin, id): Observable { return this.doPost(`${this.apiURL}/plugins/${plugin}/live`, {id}); } addItemToService(plugin, id): Observable { return this.doPost(`${this.apiURL}/plugins/${plugin}/add`, {id}); } transposeSong(transpose_value): Observable { return this.doGet(`${this.apiURL}/plugins/songs/transpose-live-item/${transpose_value}`); } login(credentials: Credentials): Observable { return this.doPost(`${this.apiURL}/core/login`, credentials); } protected doGet(url: string): Observable { return this.http.get(url, httpOptions); } protected doPost(url: string, body: any): Observable { // User is expecting instant response, so we'll accelerate the websocket reconnection process if needed. this.reconnectWebSocketIfNeeded(); return this.http.post(url, body, httpOptions); } get webSocketStatus(): WebSocketStatus { if (this.ws) { switch (this.ws.readyState) { case WebSocket.OPEN: return WebSocketStatus.Open; } } return WebSocketStatus.Closed; } reconnectWebSocketIfNeeded() { if (this.webSocketStatus === WebSocketStatus.Closed) { this.createWebSocket(); } } createWebSocket() { this.clearWebSocketTimeoutHandle(); if (this.retrieveSystemInformationSubscription) { // Cancels ongoing request to avoid connection flooding this.retrieveSystemInformationSubscription.unsubscribe(); } this.retrieveSystemInformationSubscription = this.retrieveSystemInformation() .pipe( shareReplay(1), finalize(() => this.retrieveSystemInformationSubscription = null) ) .subscribe(info => { if (this.ws) { // Removing listeners to avoid loop this.ws.onmessage = null; this.ws.onclose = null; this.ws.onerror = null; this.ws.close(); this.webSocketStateChanged$.emit(WebSocketStatus.Closed); } const ws = this.ws = new WebSocket(`ws://${this.host}:${info.websocket_port}`); ws.onopen = () => { this.webSocketStateChanged$.emit(WebSocketStatus.Open); }; ws.onmessage = this.readWebSocketMessage; ws.onerror = this.handleWebSocketError; ws.onclose = () => { this.webSocketStateChanged$.emit(WebSocketStatus.Closed); this.handleWebSocketError(); }; }, _ => this.handleWebSocketError()); } private handleWebSocketError = () => { this.clearWebSocketTimeoutHandle(); this.webSocketTimeoutHandle = setTimeout(() => { this.createWebSocket(); }, WEBSOCKET_RECONNECT_TIMEOUT); }; private clearWebSocketTimeoutHandle() { if (this.webSocketTimeoutHandle) { clearTimeout(this.webSocketTimeoutHandle); } } private readWebSocketMessage = (event: MessageEvent) => { const reader = new FileReader(); reader.onload = () => { const state = deserialize(JSON.parse(reader.result as string).results, State); this.handleStateChange(state); }; reader.readAsText(event.data); }; handleStateChange(state: State) { this.isTwelveHourTime = state.twelve; this.stateChanged$.emit(state); } }