Major change from HTML5 audio to AudioContext; Apple products now works!

This commit is contained in:
Chrystian Huot 2019-08-29 11:01:04 -04:00
parent 5bbc27853f
commit 8e4c09fde4
8 changed files with 372 additions and 373 deletions

View file

@ -1,4 +1,4 @@
# Rdio Scanner - v1.0.0 # Rdio Scanner - v1.5.0
Rdio Scanner uses the files created by [Trunk Recorder](https://github.com/robotastic/trunk-recorder) and offers them in a nice web based interface, inspired by my own personnal use of police radio scanners. Rdio Scanner uses the files created by [Trunk Recorder](https://github.com/robotastic/trunk-recorder) and offers them in a nice web based interface, inspired by my own personnal use of police radio scanners.

View file

@ -17,16 +17,16 @@ export class AppRadioControlComponent implements OnDestroy, OnInit {
systemHold = false; systemHold = false;
talkgroupHold = false; talkgroupHold = false;
private _subscriptions: Subscription[] = []; private subscriptions: Subscription[] = [];
constructor(public appRadioService: AppRadioService) { } constructor(private appRadioService: AppRadioService) { }
ngOnInit(): void { ngOnInit(): void {
this._subscribe(); this.subscribe();
} }
ngOnDestroy(): void { ngOnDestroy(): void {
this._unsubscribe(); this.unsubscribe();
} }
avoid(): void { avoid(): void {
@ -65,13 +65,13 @@ export class AppRadioControlComponent implements OnDestroy, OnInit {
this.appRadioService.liveFeed(); this.appRadioService.liveFeed();
} }
private _handlePauseEvent(event: RadioEvent): void { private handlePauseEvent(event: RadioEvent): void {
if ('pause' in event) { if ('pause' in event) {
this.paused = event.pause; this.paused = event.pause;
} }
} }
private _handleRadioEvent(event: RadioEvent): void { private handleRadioEvent(event: RadioEvent): void {
if ('hold' in event) { if ('hold' in event) {
switch (event.hold) { switch (event.hold) {
case '-sys': case '-sys':
@ -92,16 +92,16 @@ export class AppRadioControlComponent implements OnDestroy, OnInit {
} }
} }
private _subscribe(): void { private subscribe(): void {
this._subscriptions.push( this.subscriptions.push(
this.appRadioService.event.subscribe((event: RadioEvent) => { this.appRadioService.event.subscribe((event: RadioEvent) => {
this._handlePauseEvent(event); this.handlePauseEvent(event);
this._handleRadioEvent(event); this.handleRadioEvent(event);
}), }),
); );
} }
private _unsubscribe(subscriptions: Subscription[] = this._subscriptions): void { private unsubscribe(subscriptions: Subscription[] = this.subscriptions): void {
while (subscriptions.length) { while (subscriptions.length) {
subscriptions.pop().unsubscribe(); subscriptions.pop().unsubscribe();
} }

View file

@ -23,24 +23,24 @@ export class AppRadioDisplayComponent implements OnDestroy, OnInit {
live = false; live = false;
queueLength = 0; queueLength = 0;
private _clockInterval: NodeJS.Timeout; private clockInterval: NodeJS.Timeout;
private _subscriptions: Subscription[] = []; private subscriptions: Subscription[] = [];
constructor(public appRadioService: AppRadioService) { } constructor(private appRadioService: AppRadioService) { }
ngOnInit(): void { ngOnInit(): void {
this._clockInterval = setInterval(() => this.clock = new Date(), 1000); this.clockInterval = setInterval(() => this.clock = new Date(), 1000);
this._subscribe(); this.subscribe();
} }
ngOnDestroy(): void { ngOnDestroy(): void {
this._unsubscribe(); this.unsubscribe();
clearInterval(this._clockInterval); clearInterval(this.clockInterval);
} }
private _handleRadioCallEvent(event: RadioEvent): void { private handleRadioCallEvent(event: RadioEvent): void {
if ( if (
'call' in event && 'call' in event &&
event.call !== null && event.call !== null &&
@ -54,19 +54,19 @@ export class AppRadioDisplayComponent implements OnDestroy, OnInit {
} }
} }
private _handleRadioLiveEvent(event: RadioEvent): void { private handleRadioLiveEvent(event: RadioEvent): void {
if ('live' in event) { if ('live' in event) {
this.live = event.live; this.live = event.live;
} }
} }
private _handleRadioQueueEvent(event: RadioEvent): void { private handleRadioQueueEvent(event: RadioEvent): void {
if ('queue' in event) { if ('queue' in event) {
this.queueLength = event.queue; this.queueLength = event.queue;
} }
} }
private _handleRadioTimeEvent(event: RadioEvent): void { private handleRadioTimeEvent(event: RadioEvent): void {
if ('time' in event) { if ('time' in event) {
const currentFreq = this.currentCall.freqList const currentFreq = this.currentCall.freqList
.reduce((cur: RadioCallFreq, freq: RadioCallFreq) => freq.pos < event.time ? freq : cur, null); .reduce((cur: RadioCallFreq, freq: RadioCallFreq) => freq.pos < event.time ? freq : cur, null);
@ -98,18 +98,18 @@ export class AppRadioDisplayComponent implements OnDestroy, OnInit {
} }
} }
private _subscribe(): void { private subscribe(): void {
this._subscriptions.push( this.subscriptions.push(
this.appRadioService.event.subscribe((event: RadioEvent) => { this.appRadioService.event.subscribe((event: RadioEvent) => {
this._handleRadioCallEvent(event); this.handleRadioCallEvent(event);
this._handleRadioLiveEvent(event); this.handleRadioLiveEvent(event);
this._handleRadioQueueEvent(event); this.handleRadioQueueEvent(event);
this._handleRadioTimeEvent(event); this.handleRadioTimeEvent(event);
}), }),
); );
} }
private _unsubscribe(subscriptions: Subscription[] = this._subscriptions): void { private unsubscribe(subscriptions: Subscription[] = this.subscriptions): void {
while (subscriptions.length) { while (subscriptions.length) {
subscriptions.pop().unsubscribe(); subscriptions.pop().unsubscribe();
} }

View file

@ -15,38 +15,38 @@ export class AppRadioLedComponent implements OnDestroy, OnInit {
paused = false; paused = false;
rx = false; rx = false;
private _subcriptions: Subscription[] = []; private subcriptions: Subscription[] = [];
constructor(public appRadioService: AppRadioService) { } constructor(private appRadioService: AppRadioService) { }
ngOnInit(): void { ngOnInit(): void {
this._subscribe(); this.subscribe();
} }
ngOnDestroy(): void { ngOnDestroy(): void {
this._unsubscribe(); this.unsubscribe();
} }
private _handleCallEvent(event: RadioEvent): void { private handleCallEvent(event: RadioEvent): void {
if ('call' in event) { if ('call' in event) {
this.rx = event.call === null ? false : true; this.rx = event.call === null ? false : true;
} }
} }
private _handlePauseEvent(event: RadioEvent): void { private handlePauseEvent(event: RadioEvent): void {
if ('pause' in event) { if ('pause' in event) {
this.paused = event.pause; this.paused = event.pause;
} }
} }
private _subscribe(): void { private subscribe(): void {
this._subcriptions.push(this.appRadioService.event.subscribe((event: RadioEvent) => { this.subcriptions.push(this.appRadioService.event.subscribe((event: RadioEvent) => {
this._handleCallEvent(event); this.handleCallEvent(event);
this._handlePauseEvent(event); this.handlePauseEvent(event);
})); }));
} }
private _unsubscribe(subscriptions: Subscription[] = this._subcriptions): void { private unsubscribe(subscriptions: Subscription[] = this.subcriptions): void {
while (subscriptions.length) { while (subscriptions.length) {
subscriptions.pop().unsubscribe(); subscriptions.pop().unsubscribe();
} }

View file

@ -44,22 +44,18 @@ export class AppRadioSearchComponent implements OnDestroy, OnInit {
readonly columns = ['control', 'date', 'time', 'system', 'alpha', 'description']; readonly columns = ['control', 'date', 'time', 'system', 'alpha', 'description'];
get pageSize() { @ViewChild(MatPaginator, { static: true }) private matPaginator: MatPaginator;
return this._pageSize.value; @ViewChild(MatTable, { static: true }) private matTable: MatTable<RadioCall[]>;
}
@ViewChild(MatPaginator, { static: true }) private _matPaginator: MatPaginator; private active = false;
@ViewChild(MatTable, { static: true }) private _matTable: MatTable<RadioCall[]>; private filterDate: BehaviorSubject<Date> = new BehaviorSubject<Date>(DEFAULT_FILTERS.date);
private filterSystem: BehaviorSubject<number> = new BehaviorSubject<number>(DEFAULT_FILTERS.system);
private filterTalkgroup: BehaviorSubject<number> = new BehaviorSubject<number>(DEFAULT_FILTERS.talkgroup);
private pageCurrent: BehaviorSubject<number> = new BehaviorSubject<number>(1);
private pageSize: BehaviorSubject<number> = new BehaviorSubject<number>(10);
private pageSort: BehaviorSubject<number> = new BehaviorSubject<number>(DEFAULT_FILTERS.sort);
private _active = false; private subscriptions: {
private _filterDate: BehaviorSubject<Date> = new BehaviorSubject<Date>(DEFAULT_FILTERS.date);
private _filterSystem: BehaviorSubject<number> = new BehaviorSubject<number>(DEFAULT_FILTERS.system);
private _filterTalkgroup: BehaviorSubject<number> = new BehaviorSubject<number>(DEFAULT_FILTERS.talkgroup);
private _pageCurrent: BehaviorSubject<number> = new BehaviorSubject<number>(1);
private _pageSize: BehaviorSubject<number> = new BehaviorSubject<number>(10);
private _pageSort: BehaviorSubject<number> = new BehaviorSubject<number>(DEFAULT_FILTERS.sort);
private _subscriptions: {
calls: Subscription[]; calls: Subscription[];
dateLimits: Subscription[]; dateLimits: Subscription[];
form: Subscription[]; form: Subscription[];
@ -80,12 +76,12 @@ export class AppRadioSearchComponent implements OnDestroy, OnInit {
} }
ngOnInit(): void { ngOnInit(): void {
this._subscribeFormChanges(); this.subscribeFormChanges();
this._subscribeRadioEvent(); this.subscribeRadioEvent();
} }
ngOnDestroy(): void { ngOnDestroy(): void {
this._unsubscribe(); this.unsubscribe();
} }
close(): void { close(): void {
@ -93,7 +89,7 @@ export class AppRadioSearchComponent implements OnDestroy, OnInit {
} }
onPage(event: PageEvent): void { onPage(event: PageEvent): void {
this._pageCurrent.next(event.pageIndex + 1); this.pageCurrent.next(event.pageIndex + 1);
} }
play(call: RadioCall): void { play(call: RadioCall): void {
@ -101,7 +97,7 @@ export class AppRadioSearchComponent implements OnDestroy, OnInit {
if (call.audio) { if (call.audio) {
this.appRadioService.play(call); this.appRadioService.play(call);
} else { } else {
this._loadAudio(call).then(() => this.appRadioService.play(call)); this.loadAudio(call).then(() => this.appRadioService.play(call));
} }
} }
} }
@ -110,27 +106,27 @@ export class AppRadioSearchComponent implements OnDestroy, OnInit {
this.form.reset(DEFAULT_FILTERS); this.form.reset(DEFAULT_FILTERS);
} }
private _handleSearchEvent(event: RadioEvent): void { private handleSearchEvent(event: RadioEvent): void {
if ('search' in event) { if ('search' in event) {
this._active = !this._active; this.active = !this.active;
if (this._active) { if (this.active) {
this._subscribeCalls(); this.subscribeCalls();
this._subscribeDateLimits(); this.subscribeDateLimits();
} else { } else {
this._unsubscribeCalls(); this.unsubscribeCalls();
this._unsubscribeDateLimits(); this.unsubscribeDateLimits();
} }
} }
} }
private _handleSystemsEvent(event: RadioEvent): void { private handleSystemsEvent(event: RadioEvent): void {
if ('systems' in event) { if ('systems' in event) {
this.systems.splice(0, this.systems.length, ...event.systems); this.systems.splice(0, this.systems.length, ...event.systems);
} }
} }
private _loadAudio(call: RadioCall): Promise<RadioCall> { private loadAudio(call: RadioCall): Promise<RadioCall> {
return new Promise((resolve) => { return new Promise((resolve) => {
if (call) { if (call) {
MeteorObservable.call('call-audio', call._id).subscribe((audio: string) => { MeteorObservable.call('call-audio', call._id).subscribe((audio: string) => {
@ -143,20 +139,20 @@ export class AppRadioSearchComponent implements OnDestroy, OnInit {
}); });
} }
private _subscribeCalls(): void { private subscribeCalls(): void {
const subscriptions: Subscription[] = []; const subscriptions: Subscription[] = [];
this._subscriptions.calls.push(combineLatest( this.subscriptions.calls.push(combineLatest(
this._filterDate, this.filterDate,
this._filterSystem, this.filterSystem,
this._filterTalkgroup, this.filterTalkgroup,
this._pageCurrent, this.pageCurrent,
this._pageSize, this.pageSize,
this._pageSort, this.pageSort,
) )
.pipe( .pipe(
debounceTime(100), debounceTime(100),
finalize(() => this._unsubscribe(subscriptions)), finalize(() => this.unsubscribe(subscriptions)),
) )
.subscribe(([ .subscribe(([
date, date,
@ -191,7 +187,7 @@ export class AppRadioSearchComponent implements OnDestroy, OnInit {
selector.talkgroup = talkgroup; selector.talkgroup = talkgroup;
} }
this._unsubscribe(subscriptions); this.unsubscribe(subscriptions);
subscriptions.push(MeteorObservable.subscribe('calls', selector, { subscriptions.push(MeteorObservable.subscribe('calls', selector, {
fields: { fields: {
@ -211,7 +207,7 @@ export class AppRadioSearchComponent implements OnDestroy, OnInit {
.subscribe((calls: RadioCall[]) => { .subscribe((calls: RadioCall[]) => {
this.calls.splice(0, this.calls.length, ...calls); this.calls.splice(0, this.calls.length, ...calls);
this._matTable.renderRows(); this.matTable.renderRows();
MeteorObservable.call('calls-count', selector).subscribe((count: number) => this.count = count); MeteorObservable.call('calls-count', selector).subscribe((count: number) => this.count = count);
})); }));
@ -219,8 +215,8 @@ export class AppRadioSearchComponent implements OnDestroy, OnInit {
})); }));
} }
private _subscribeDateLimits(): void { private subscribeDateLimits(): void {
this._subscriptions.dateLimits.push( this.subscriptions.dateLimits.push(
MeteorObservable.subscribe('calls', {}, { MeteorObservable.subscribe('calls', {}, {
fields: { audio: 0 }, fields: { audio: 0 },
@ -253,17 +249,17 @@ export class AppRadioSearchComponent implements OnDestroy, OnInit {
); );
} }
private _subscribeFormChanges(): void { private subscribeFormChanges(): void {
const dateForm = this.form.get('date'); const dateForm = this.form.get('date');
const sortForm = this.form.get('sort'); const sortForm = this.form.get('sort');
const systemForm = this.form.get('system'); const systemForm = this.form.get('system');
const talkgroupForm = this.form.get('talkgroup'); const talkgroupForm = this.form.get('talkgroup');
this._subscriptions.form.push(dateForm.valueChanges.subscribe((date: Date) => { this.subscriptions.form.push(dateForm.valueChanges.subscribe((date: Date) => {
this._filterDate.next(date); this.filterDate.next(date);
})); }));
this._subscriptions.form.push(systemForm.valueChanges.subscribe((sys: number) => { this.subscriptions.form.push(systemForm.valueChanges.subscribe((sys: number) => {
if (sys === -1) { if (sys === -1) {
this.talkgroups.splice(0, this.talkgroups.length); this.talkgroups.splice(0, this.talkgroups.length);
@ -285,44 +281,44 @@ export class AppRadioSearchComponent implements OnDestroy, OnInit {
talkgroupForm.reset(-1); talkgroupForm.reset(-1);
this._filterSystem.next(sys); this.filterSystem.next(sys);
this._matPaginator.firstPage(); this.matPaginator.firstPage();
})); }));
this._subscriptions.form.push(talkgroupForm.valueChanges.subscribe((tg: number) => { this.subscriptions.form.push(talkgroupForm.valueChanges.subscribe((tg: number) => {
this._filterTalkgroup.next(tg); this.filterTalkgroup.next(tg);
this._matPaginator.firstPage(); this.matPaginator.firstPage();
})); }));
this._subscriptions.form.push(sortForm.valueChanges.subscribe((sort: number) => { this.subscriptions.form.push(sortForm.valueChanges.subscribe((sort: number) => {
this._pageSort.next(sort); this.pageSort.next(sort);
this._matPaginator.firstPage(); this.matPaginator.firstPage();
})); }));
} }
private _subscribeRadioEvent(): void { private subscribeRadioEvent(): void {
this._subscriptions.radio.push(this.appRadioService.event.subscribe((event: RadioEvent) => { this.subscriptions.radio.push(this.appRadioService.event.subscribe((event: RadioEvent) => {
this._handleSearchEvent(event); this.handleSearchEvent(event);
this._handleSystemsEvent(event); this.handleSystemsEvent(event);
})); }));
} }
private _unsubscribe(subscriptions: Subscription[] = [ private unsubscribe(subscriptions: Subscription[] = [
...this._subscriptions.calls, ...this.subscriptions.calls,
...this._subscriptions.dateLimits, ...this.subscriptions.dateLimits,
...this._subscriptions.form, ...this.subscriptions.form,
...this._subscriptions.radio, ...this.subscriptions.radio,
]): void { ]): void {
while (subscriptions.length) { while (subscriptions.length) {
subscriptions.pop().unsubscribe(); subscriptions.pop().unsubscribe();
} }
} }
private _unsubscribeCalls(): void { private unsubscribeCalls(): void {
this._unsubscribe(this._subscriptions.calls); this.unsubscribe(this.subscriptions.calls);
} }
private _unsubscribeDateLimits(): void { private unsubscribeDateLimits(): void {
this._unsubscribe(this._subscriptions.dateLimits); this.unsubscribe(this.subscriptions.dateLimits);
} }
} }

View file

@ -16,18 +16,18 @@ export class AppRadioSelectComponent implements OnDestroy, OnInit {
systems: RadioSystem[] = []; systems: RadioSystem[] = [];
private _subscriptions: Subscription[] = []; private subscriptions: Subscription[] = [];
constructor(public appRadioService: AppRadioService) { constructor(private appRadioService: AppRadioService) {
this.avoids = this.appRadioService.getAvoids(); this.avoids = this.appRadioService.getAvoids();
} }
ngOnInit(): void { ngOnInit(): void {
this._subscribe(); this.subscribe();
} }
ngOnDestroy(): void { ngOnDestroy(): void {
this._unsubscribe(); this.unsubscribe();
} }
avoid(system: RadioSystem | null, talkgroup: RadioTalkgroup | null, status: boolean): void { avoid(system: RadioSystem | null, talkgroup: RadioTalkgroup | null, status: boolean): void {
@ -42,34 +42,34 @@ export class AppRadioSelectComponent implements OnDestroy, OnInit {
this.appRadioService.select(); this.appRadioService.select();
} }
private _handleAvoidEvent(event: RadioEvent): void { private handleAvoidEvent(event: RadioEvent): void {
if ('avoid' in event) { if ('avoid' in event) {
this.avoids[event.avoid.sys][event.avoid.tg] = event.avoid.status; this.avoids[event.avoid.sys][event.avoid.tg] = event.avoid.status;
} }
} }
private _handleAvoidsEvent(event: RadioEvent): void { private handleAvoidsEvent(event: RadioEvent): void {
if ('avoids' in event) { if ('avoids' in event) {
this.avoids = event.avoids; this.avoids = event.avoids;
} }
} }
private _handleSystemsEvent(event: RadioEvent): void { private handleSystemsEvent(event: RadioEvent): void {
if ('systems' in event) { if ('systems' in event) {
this.systems.splice(0, this.systems.length, ...event.systems); this.systems.splice(0, this.systems.length, ...event.systems);
} }
} }
private _subscribe(): void { private subscribe(): void {
this._subscriptions.push( this.subscriptions.push(
this.appRadioService.event.subscribe((event: RadioEvent) => { this.appRadioService.event.subscribe((event: RadioEvent) => {
this._handleAvoidEvent(event); this.handleAvoidEvent(event);
this._handleSystemsEvent(event); this.handleSystemsEvent(event);
}) })
); );
} }
private _unsubscribe(subscriptions: Subscription[] = this._subscriptions) { private unsubscribe(subscriptions: Subscription[] = this.subscriptions) {
while (subscriptions.length) { while (subscriptions.length) {
subscriptions.pop().unsubscribe(); subscriptions.pop().unsubscribe();
} }

View file

@ -15,28 +15,28 @@ export class AppRadioComponent implements OnDestroy, OnInit {
searchPanelOpened = false; searchPanelOpened = false;
selectPanelOpened = false; selectPanelOpened = false;
private _subscriptions: Subscription[] = []; private subscriptions: Subscription[] = [];
constructor(public appRadioService: AppRadioService) { } constructor(private appRadioService: AppRadioService) { }
ngOnInit(): void { ngOnInit(): void {
this._subscribe(); this.subscribe();
} }
ngOnDestroy(): void { ngOnDestroy(): void {
this._unsubscribe(); this.unsubscribe();
} }
@HostListener('window:beforeunload', ['$event']) @HostListener('window:beforeunload', ['$event'])
private _exitNotification(event: BeforeUnloadEvent): void { private exitNotification(event: BeforeUnloadEvent): void {
if (this.appRadioService.live) { if (this.appRadioService.live) {
event.preventDefault(); event.preventDefault();
event.returnValue = false; event.returnValue = false;
} }
} }
private _subscribe(): void { private subscribe(): void {
this._subscriptions.push( this.subscriptions.push(
this.appRadioService.event.subscribe((event: RadioEvent) => { this.appRadioService.event.subscribe((event: RadioEvent) => {
if ('search' in event) { if ('search' in event) {
this.searchPanelOpened = !this.searchPanelOpened; this.searchPanelOpened = !this.searchPanelOpened;
@ -50,7 +50,7 @@ export class AppRadioComponent implements OnDestroy, OnInit {
); );
} }
private _unsubscribe(subscriptions: Subscription[] = this._subscriptions): void { private unsubscribe(subscriptions: Subscription[] = this.subscriptions): void {
while (subscriptions.length) { while (subscriptions.length) {
subscriptions.pop().unsubscribe(); subscriptions.pop().unsubscribe();
} }

View file

@ -4,6 +4,10 @@ import { Subscription } from 'rxjs';
import { debounceTime, filter, skip } from 'rxjs/operators'; import { debounceTime, filter, skip } from 'rxjs/operators';
import { RadioAvoids, RadioCall, RadioCalls, RadioEvent, RadioSystem, RadioSystems, RadioTalkgroup } from './radio'; import { RadioAvoids, RadioCall, RadioCalls, RadioEvent, RadioSystem, RadioSystems, RadioTalkgroup } from './radio';
declare var webkitAudioContext: any;
const EVENTS = ['mousedown', 'touchdown'];
const LOCAL_STORAGE_KEY = { const LOCAL_STORAGE_KEY = {
avoids: 'radio-avoids', avoids: 'radio-avoids',
}; };
@ -13,14 +17,20 @@ export class AppRadioService implements OnDestroy {
readonly event = new EventEmitter<RadioEvent>(); readonly event = new EventEmitter<RadioEvent>();
get live() { get live() {
return !!this._subscriptions.liveFeed.length; return !!this.subscriptions.liveFeed.length;
} }
private _audio: HTMLAudioElement; private audioContext: AudioContext;
private _avoids: RadioAvoids = {}; private audioSource: AudioBufferSourceNode = null;
private _call: { private audioStartTime = NaN;
private audioTimer: any = null;
private avoids: RadioAvoids = {};
private call: {
current: RadioCall | null; current: RadioCall | null;
previous: RadioCall | null; previous: RadioCall | null;
queue: RadioCall[]; queue: RadioCall[];
@ -30,7 +40,7 @@ export class AppRadioService implements OnDestroy {
queue: [], queue: [],
}; };
private _hold: { private hold: {
system: RadioSystem | number | null, system: RadioSystem | number | null,
talkgroup: RadioTalkgroup | number | null, talkgroup: RadioTalkgroup | number | null,
} = { } = {
@ -38,9 +48,9 @@ export class AppRadioService implements OnDestroy {
talkgroup: null, talkgroup: null,
}; };
private _paused = false; private paused = false;
private _subscriptions: { private subscriptions: {
liveFeed: Subscription[]; liveFeed: Subscription[];
systems: Subscription[]; systems: Subscription[];
} = { } = {
@ -48,18 +58,20 @@ export class AppRadioService implements OnDestroy {
systems: [], systems: [],
}; };
private _systems: RadioSystem[] = []; private systems: RadioSystem[] = [];
constructor() { constructor() {
this._readAvoids(); this.bootstrap();
this._subscribeSystems(); this.readAvoids();
this.subscribeSystems();
} }
ngOnDestroy(): void { ngOnDestroy(): void {
this.event.complete(); this.event.complete();
this._unsubscribe(); this.unsubscribe();
} }
avoid(parms: { avoid(parms: {
@ -70,183 +82,206 @@ export class AppRadioService implements OnDestroy {
status?: boolean status?: boolean
} = {}): void { } = {}): void {
if (typeof parms.all === 'boolean') { if (typeof parms.all === 'boolean') {
Object.keys(this._avoids).map((sys: string) => +sys).forEach((sys: number) => { Object.keys(this.avoids).map((sys: string) => +sys).forEach((sys: number) => {
Object.keys(this._avoids[sys]).map((tg: string) => +tg).forEach((tg: number) => { Object.keys(this.avoids[sys]).map((tg: string) => +tg).forEach((tg: number) => {
this._avoids[sys][tg] = typeof parms.status === 'boolean' ? parms.status : !this._avoids[sys][tg]; this.avoids[sys][tg] = typeof parms.status === 'boolean' ? parms.status : !this.avoids[sys][tg];
this._emitAvoidEvent(sys, tg); this.emitAvoidEvent(sys, tg);
}); });
}); });
} else if (parms.call) { } else if (parms.call) {
const sys = parms.call.system; const sys = parms.call.system;
const tg = parms.call.talkgroup; const tg = parms.call.talkgroup;
this._avoids[sys][tg] = typeof parms.status === 'boolean' ? parms.status : !this._avoids[sys][tg]; this.avoids[sys][tg] = typeof parms.status === 'boolean' ? parms.status : !this.avoids[sys][tg];
this._emitAvoidEvent(sys, tg); this.emitAvoidEvent(sys, tg);
} else if (parms.system && parms.talkgroup) { } else if (parms.system && parms.talkgroup) {
const sys = parms.system.system; const sys = parms.system.system;
const tg = parms.talkgroup.dec; const tg = parms.talkgroup.dec;
this._avoids[sys][tg] = typeof parms.status === 'boolean' ? parms.status : !this._avoids[sys][tg]; this.avoids[sys][tg] = typeof parms.status === 'boolean' ? parms.status : !this.avoids[sys][tg];
this._emitAvoidEvent(sys, tg); this.emitAvoidEvent(sys, tg);
} else if (parms.system && !parms.talkgroup) { } else if (parms.system && !parms.talkgroup) {
const sys = parms.system.system; const sys = parms.system.system;
Object.keys(this._avoids[sys]).map((tg: string) => +tg).forEach((tg: number) => { Object.keys(this.avoids[sys]).map((tg: string) => +tg).forEach((tg: number) => {
this._avoids[sys][tg] = typeof parms.status === 'boolean' ? parms.status : !this._avoids[sys][tg]; this.avoids[sys][tg] = typeof parms.status === 'boolean' ? parms.status : !this.avoids[sys][tg];
this._emitAvoidEvent(sys, tg); this.emitAvoidEvent(sys, tg);
}); });
} else { } else {
const call = this._call.current || this._call.previous; const call = this.call.current || this.call.previous;
if (call) { if (call) {
const sys = call.system; const sys = call.system;
const tg = call.talkgroup; const tg = call.talkgroup;
this._avoids[sys][tg] = typeof parms.status === 'boolean' ? parms.status : !this._avoids[sys][tg]; this.avoids[sys][tg] = typeof parms.status === 'boolean' ? parms.status : !this.avoids[sys][tg];
this._emitAvoidEvent(sys, tg); this.emitAvoidEvent(sys, tg);
} }
} }
this._cleanQueue(); this.cleanQueue();
if (this._call.current && this._callFiltered()) { if (this.call.current && this.callFiltered()) {
this.skip(); this.skip();
} }
if (this.live) { if (this.live) {
this._unsubscribeLiveFeed(); this.unsubscribeLiveFeed();
this._subscribeLiveFeed(); this.subscribeLiveFeed();
} }
this._writeAvoids(); this.writeAvoids();
} }
getAvoids(): RadioAvoids { getAvoids(): RadioAvoids {
return this._avoids; return this.avoids;
} }
holdSystem(): void { holdSystem(): void {
const call = this._call.current || this._call.previous; const call = this.call.current || this.call.previous;
if (call) { if (call) {
this._hold.system = this._hold.system === null ? call.systemData || call.system : null; this.hold.system = this.hold.system === null ? call.systemData || call.system : null;
this._emitHoldEvent(this._hold.system === null ? '-sys' : '+sys'); this.emitHoldEvent(this.hold.system === null ? '-sys' : '+sys');
if (this._hold.system !== null) { if (this.hold.system !== null) {
this._cleanQueue(); this.cleanQueue();
} }
} }
} }
holdTalkgroup(): void { holdTalkgroup(): void {
const call = this._call.current || this._call.previous; const call = this.call.current || this.call.previous;
if (call) { if (call) {
this._hold.talkgroup = this._hold.talkgroup === null ? call.talkgroupData || call.talkgroup : null; this.hold.talkgroup = this.hold.talkgroup === null ? call.talkgroupData || call.talkgroup : null;
this._emitHoldEvent(this._hold.talkgroup === null ? '-tg' : '+tg'); this.emitHoldEvent(this.hold.talkgroup === null ? '-tg' : '+tg');
if (this._hold.talkgroup !== null) { if (this.hold.talkgroup !== null) {
this._cleanQueue(); this.cleanQueue();
} }
} }
} }
liveFeed(status = !this.live): void { liveFeed(status = !this.live): void {
this._configureAudio();
if (status) { if (status) {
this._subscribeLiveFeed(); this.subscribeLiveFeed();
} else { } else {
this._unsubscribeLiveFeed(); this.unsubscribeLiveFeed();
this._emitLiveEvent(); this.emitLiveEvent();
this._flushQueue(); this.flushQueue();
this.stop(); this.stop();
} }
} }
pause(): void { pause(): void {
this._configureAudio(); this.paused = !this.paused;
this._paused = !this._paused; this.emitPauseEvent();
this._emitPauseEvent();
if (this._paused) {
this._audio.pause();
} else {
if (this._call.current) {
this._audio.play();
if (this.audioContext) {
if (this.paused) {
this.audioContext.suspend();
} else { } else {
this.skip(true); this.audioContext.resume();
this.play();
} }
} }
} }
play(call?: RadioCall): void { play(call?: RadioCall): void {
this._configureAudio(); if (this.audioContext && !this.paused) {
if (!call && !this.call.current && this.call.queue.length) {
call = this.call.queue.shift();
this.emitQueueEvent();
}
if (!this._paused) { if (call && call.audio) {
if (call) { this.call.previous = this.call.current;
if (call.audio) { this.call.current = call;
if (this._call.current) {
this._call.queue.unshift(call);
this.skip(true); this.emitCallEvent();
} else { const audio = /^data:[^;]+;base64,/.test(call.audio) ? atob(call.audio.replace(/^[^,]*,/, '')) : call.audio;
this._call.current = call; const arrayBuffer = new ArrayBuffer(audio.length);
const arrayBufferView = new Uint8Array(arrayBuffer);
this._audio.src = call.audio; for (let i = 0; i < audio.length; i++) {
this._audio.load(); arrayBufferView[i] = audio.charCodeAt(i);
}
} }
} else if (!this._call.current && this._call.queue.length && !this._paused) { this.audioContext.decodeAudioData(arrayBuffer, (buffer) => {
this._call.current = this._call.queue.shift(); if (this.audioSource) {
this.audioSource.disconnect();
}
this._audio.src = this._call.current.audio; this.audioSource = this.audioContext.createBufferSource();
this._audio.load();
this.audioSource.buffer = buffer;
this.audioSource.connect(this.audioContext.destination);
this.audioSource.onended = () => this.skip();
this.audioSource.start();
if (this.audioContext.state === 'suspended') {
const resume = () => {
this.audioContext.resume();
setTimeout(() => {
if (this.audioContext.state === 'running') {
EVENTS.forEach((event) => document.body.removeEventListener(event, resume));
}
}, 0);
};
EVENTS.forEach((event) => document.body.addEventListener(event, resume));
}
this.emitTimeEvent();
this.audioTimer = setInterval(() => this.emitTimeEvent(), 500);
}, (error: Error) => {
throw (error);
this.skip(true);
});
} }
} }
} }
queue(call: RadioCall | RadioCall[], bypass = false): void {
queue(call: RadioCall | RadioCall[]): void {
if (Array.isArray(call)) { if (Array.isArray(call)) {
call.forEach((_call: RadioCall) => this.queue(_call)); call.forEach((_call: RadioCall) => this.queue(_call));
} else if (bypass || !this._callFiltered(call)) { } else if (!this.callFiltered(call)) {
this._call.queue.push(call); this.call.queue.push(call);
this._emitQueueEvent(); this.emitQueueEvent();
this.play(); this.play();
} }
} }
replay(): void { replay(): void {
if (!this._paused) { if (!this.paused) {
const call = this._call.current || this._call.previous; this.play(this.call.current || this.call.previous);
if (call) {
this.play(call);
}
} }
} }
search(): void { search(): void {
this._emitSearchEvent(); this.emitSearchEvent();
} }
select(): void { select(): void {
this._emitSelectEvent(); this.emitSelectEvent();
} }
skip(nodelay = false): void { skip(nodelay = false): void {
@ -254,205 +289,173 @@ export class AppRadioService implements OnDestroy {
if (nodelay) { if (nodelay) {
this.play(); this.play();
} else { } else {
setTimeout(() => this.play(), 1000); setTimeout(() => this.play(), 1000);
} }
} }
stop(): void { stop(): void {
if (this._call.current) { if (this.audioTimer !== null) {
this._audio.src = this._getEmptyAudioStream(); clearInterval(this.audioTimer);
this.audioTimer = null;
this._call.previous = this._call.current;
this._call.current = null;
this._emitCallEvent();
} }
if (this.audioSource) {
this.audioSource.disconnect();
this.audioSource = null;
}
this.audioStartTime = NaN;
if (this.call.current) {
this.call.previous = this.call.current;
this.call.current = null;
}
this.emitCallEvent();
} }
transform(call: RadioCall): RadioCall { transform(call: RadioCall): RadioCall {
call.systemData = (call && this._systems call.systemData = (call && this.systems
.find((_system: RadioSystem) => _system.system === call.system)) || null; .find((system: RadioSystem) => system.system === call.system)) || null;
call.talkgroupData = (call && call.systemData && call.systemData.talkgroups call.talkgroupData = (call && call.systemData && call.systemData.talkgroups
.find((_talkgroup: RadioTalkgroup) => _talkgroup.dec === call.talkgroup)) || null; .find((talkgroup: RadioTalkgroup) => talkgroup.dec === call.talkgroup)) || null;
return call; return call;
} }
private _callFiltered(call: RadioCall = this._call.current): boolean { private bootstrap(): void {
const bootstrap = () => {
if (!this.audioContext) {
if ('webkitAudioContext' in window) {
this.audioContext = new webkitAudioContext();
} else {
this.audioContext = new AudioContext();
}
if (this.audioContext.state === 'suspended') {
this.audioContext.resume();
}
}
EVENTS.forEach((event) => document.body.removeEventListener(event, bootstrap));
};
EVENTS.forEach((event) => document.body.addEventListener(event, bootstrap));
}
private callFiltered(call: RadioCall = this.call.current): boolean {
if ( if (
this._hold.system !== null && this.hold.system !== null &&
(typeof this._hold.system === 'object' && this._hold.system !== call.systemData) || (typeof this.hold.system === 'object' && this.hold.system !== call.systemData) ||
(typeof this._hold.system === 'number' && this._hold.system !== call.system) (typeof this.hold.system === 'number' && this.hold.system !== call.system)
) { ) {
return true; return true;
} else if ( } else if (
this._hold.talkgroup !== null && this.hold.talkgroup !== null &&
(typeof this._hold.talkgroup === 'object' && this._hold.talkgroup !== call.talkgroupData) || (typeof this.hold.talkgroup === 'object' && this.hold.talkgroup !== call.talkgroupData) ||
(typeof this._hold.talkgroup === 'number' && this._hold.talkgroup !== call.talkgroup) (typeof this.hold.talkgroup === 'number' && this.hold.talkgroup !== call.talkgroup)
) { ) {
return true; return true;
} else { } else {
const sys = call.system; const sys = call.system;
const tg = call.talkgroup; const tg = call.talkgroup;
const avoided = this._avoids[sys] && this._avoids[sys][tg]; const avoided = this.avoids[sys] && this.avoids[sys][tg];
return typeof avoided === 'boolean' ? avoided : false; return typeof avoided === 'boolean' ? avoided : false;
} }
} }
private _cleanQueue(): void { private cleanQueue(): void {
const queueLength = this._call.queue.length; const queueLength = this.call.queue.length;
this._call.queue = this._call.queue.filter((_call: RadioCall) => !this._callFiltered(_call)); this.call.queue = this.call.queue.filter((call: RadioCall) => !this.callFiltered(call));
if (queueLength !== this._call.queue.length) { if (queueLength !== this.call.queue.length) {
this._emitQueueEvent(); this.emitQueueEvent();
} }
} }
private _configureAudio(): void { private emitAvoidEvent(sys: number, tg: number): void {
if (!this._audio) { this.event.emit({ avoid: { sys, tg, status: this.avoids[sys][tg] } });
let playing = false;
let progress = 0;
let watchdog = null;
const progressHandler = () => {
if (this._call.current && playing) {
// Hack for audio.currentTime that sometime returns unreliable values
if (this._audio.currentTime - progress < 5) {
this._emitTimeEvent();
}
progress = this._audio.currentTime;
if (watchdog) {
clearTimeout(watchdog);
}
// Hack for IOS devices that don't always send stop events
watchdog = setTimeout(() => stopHandler(), 1000);
}
};
const startHandler = () => {
if (this._call.current) {
playing = true;
this._emitCallEvent();
this._emitQueueEvent();
}
};
const stopHandler = () => {
if (this._call.current && playing && !this._paused) {
playing = false;
progress = 0;
if (watchdog) {
clearTimeout(watchdog);
watchdog = null;
}
this.skip();
}
};
this._audio = new Audio(this._getEmptyAudioStream());
this._audio.autoplay = true;
this._audio.onabort = () => stopHandler();
this._audio.onended = () => stopHandler();
this._audio.onerror = () => stopHandler();
this._audio.onpause = () => stopHandler();
this._audio.onplay = () => startHandler();
this._audio.onstalled = () => this._audio.play();
this._audio.onsuspend = () => this._audio.play();
this._audio.ontimeupdate = () => progressHandler();
}
} }
private _emitAvoidEvent(sys: number, tg: number): void { private emitAvoidsEvent(): void {
this.event.emit({ avoid: { sys, tg, status: this._avoids[sys][tg] } }); this.event.emit({ avoids: this.avoids });
} }
private _emitAvoidsEvent(): void { private emitCallEvent(): void {
this.event.emit({ avoids: this._avoids }); this.event.emit({ call: this.call.current });
} }
private _emitCallEvent(): void { private emitHoldEvent(value: RadioEvent['hold']): void {
this.event.emit({ call: this._call.current });
}
private _emitHoldEvent(value: RadioEvent['hold']): void {
this.event.emit({ hold: value }); this.event.emit({ hold: value });
} }
private _emitLiveEvent(): void { private emitLiveEvent(): void {
this.event.emit({ live: this.live }); this.event.emit({ live: this.live });
} }
private _emitPauseEvent(): void { private emitPauseEvent(): void {
this.event.emit({ pause: this._paused }); this.event.emit({ pause: this.paused });
} }
private _emitQueueEvent(): void { private emitQueueEvent(): void {
this.event.emit({ queue: this._call.queue.length }); this.event.emit({ queue: this.call.queue.length });
} }
private _emitSearchEvent(): void { private emitSearchEvent(): void {
this.event.emit({ search: null }); this.event.emit({ search: null });
} }
private _emitSelectEvent(): void { private emitSelectEvent(): void {
this.event.emit({ select: null }); this.event.emit({ select: null });
} }
private _emitSystemsEvent(): void { private emitSystemsEvent(): void {
this.event.emit({ systems: this._systems }); this.event.emit({ systems: this.systems });
} }
private _emitTimeEvent(): void { private emitTimeEvent(): void {
this.event.emit({ time: this._audio.currentTime }); if (!this.paused && !isNaN(this.audioContext.currentTime)) {
if (isNaN(this.audioStartTime)) {
this.audioStartTime = this.audioContext.currentTime;
}
this.event.emit({ time: this.audioContext.currentTime - this.audioStartTime });
}
} }
private _flushQueue() { private flushQueue() {
this._call.queue.splice(0, this._call.queue.length); this.call.queue.splice(0, this.call.queue.length);
this._emitQueueEvent(); this.emitQueueEvent();
} }
private _getEmptyAudioStream(): string { private readAvoids(): void {
return 'data:audio/aac;base64,//FsQAOf/N4CAExhdmM1OC4zNS4xMDAAAjBADv/xbEADn/zeAgBMYXZjNTguMzUuMTAwAAIwQA4='; const avoids = this.readLocalStorage(LOCAL_STORAGE_KEY.avoids);
}
private _readAvoids(): void {
const avoids = this._readLocalStorage(LOCAL_STORAGE_KEY.avoids);
if (avoids) { if (avoids) {
Object.keys(avoids).map((sys: string) => +sys).forEach((sys: number) => { Object.keys(avoids).map((sys: string) => +sys).forEach((sys: number) => {
if (Array.isArray(avoids[sys])) { if (Array.isArray(avoids[sys])) {
avoids[sys].forEach((tg: number) => { avoids[sys].forEach((tg: number) => {
if (!this._avoids[sys]) { if (!this.avoids[sys]) {
this._avoids[sys] = {}; this.avoids[sys] = {};
} }
this._avoids[sys][tg] = true; this.avoids[sys][tg] = true;
}); });
} }
}); });
} }
this._emitAvoidsEvent(); this.emitAvoidsEvent();
} }
private _readLocalStorage(key: string): any { private readLocalStorage(key: string): any {
if (window instanceof Window && window.localStorage instanceof Storage) { if (window instanceof Window && window.localStorage instanceof Storage) {
try { try {
return JSON.parse(window.localStorage.getItem(key)); return JSON.parse(window.localStorage.getItem(key));
@ -462,22 +465,22 @@ export class AppRadioService implements OnDestroy {
} }
} }
private _subscribeLiveFeed(): void { private subscribeLiveFeed(): void {
if (!this._subscriptions.liveFeed.length) { if (!this.subscriptions.liveFeed.length) {
const options = { const options = {
limit: 1, limit: 1,
sort: { sort: {
createdAt: -1 createdAt: -1,
}, },
transform: (call: RadioCall) => this.transform(call), transform: (call: RadioCall) => this.transform(call),
}; };
const avoids = Object.keys(this._avoids) const avoids = Object.keys(this.avoids)
.map((sys: string) => +sys) .map((sys: string) => +sys)
.reduce((sel: any, sys: number) => { .reduce((sel: any, sys: number) => {
const tgs = Object.keys(this._avoids[sys]) const tgs = Object.keys(this.avoids[sys])
.map((tg: string) => +tg) .map((tg: string) => +tg)
.filter((tg: number) => this._avoids[sys][tg]); .filter((tg: number) => this.avoids[sys][tg]);
if (tgs.length) { if (tgs.length) {
sel.push({ sel.push({
@ -497,10 +500,10 @@ export class AppRadioService implements OnDestroy {
const selector = avoids.length ? { $or: avoids } : {}; const selector = avoids.length ? { $or: avoids } : {};
this._subscriptions.liveFeed.push(MeteorObservable this.subscriptions.liveFeed.push(MeteorObservable
.subscribe('calls', selector, options) .subscribe('calls', selector, options)
.subscribe(() => { .subscribe(() => {
this._subscriptions.liveFeed.push(RadioCalls this.subscriptions.liveFeed.push(RadioCalls
.find(selector, options) .find(selector, options)
.pipe( .pipe(
filter((calls: RadioCall[]) => !!calls.length), filter((calls: RadioCall[]) => !!calls.length),
@ -509,17 +512,17 @@ export class AppRadioService implements OnDestroy {
.subscribe((calls: RadioCall[]) => this.queue(calls))); .subscribe((calls: RadioCall[]) => this.queue(calls)));
})); }));
this._emitLiveEvent(); this.emitLiveEvent();
} }
} }
private _subscribeSystems(): void { private subscribeSystems(): void {
if (!this._subscriptions.systems.length) { if (!this.subscriptions.systems.length) {
this._subscriptions.systems.push(MeteorObservable this.subscriptions.systems.push(MeteorObservable
.subscribe('systems') .subscribe('systems')
.subscribe()); .subscribe());
this._subscriptions.systems.push(RadioSystems this.subscriptions.systems.push(RadioSystems
.find({}, { .find({}, {
sort: { sort: {
system: 1, system: 1,
@ -528,71 +531,71 @@ export class AppRadioService implements OnDestroy {
.pipe(debounceTime(100)) .pipe(debounceTime(100))
.subscribe((systems: RadioSystem[]) => { .subscribe((systems: RadioSystem[]) => {
if (systems.length) { if (systems.length) {
this._systems.splice(0, this._systems.length, ...systems); this.systems.splice(0, this.systems.length, ...systems);
this._emitSystemsEvent(); this.emitSystemsEvent();
this._syncAvoids(); this.syncAvoids();
this._writeAvoids(); this.writeAvoids();
} }
})); }));
} }
} }
private _syncAvoids(): void { private syncAvoids(): void {
this._systems.forEach((system: RadioSystem) => { this.systems.forEach((system: RadioSystem) => {
const sys = system.system; const sys = system.system;
if (typeof this._avoids[sys] !== 'object') { if (typeof this.avoids[sys] !== 'object') {
this._avoids[sys] = {}; this.avoids[sys] = {};
} }
system.talkgroups.forEach((talkgroup: RadioTalkgroup) => { system.talkgroups.forEach((talkgroup: RadioTalkgroup) => {
const tg = talkgroup.dec; const tg = talkgroup.dec;
if (typeof this._avoids[sys][tg] !== 'boolean') { if (typeof this.avoids[sys][tg] !== 'boolean') {
this._avoids[sys][tg] = false; this.avoids[sys][tg] = false;
} }
}); });
}); });
Object.keys(this._avoids).map((sys: string) => +sys).forEach((sys: number) => { Object.keys(this.avoids).map((sys: string) => +sys).forEach((sys: number) => {
const system = this._systems.find((_system: RadioSystem) => _system.system === +sys); const system = this.systems.find((_system: RadioSystem) => _system.system === +sys);
if (system) { if (system) {
Object.keys(this._avoids[sys]).map((tg: string) => +tg).forEach((tg: number) => { Object.keys(this.avoids[sys]).map((tg: string) => +tg).forEach((tg: number) => {
const talkgroup = system.talkgroups.find((_talkgroup: RadioTalkgroup) => _talkgroup.dec === +tg); const talkgroup = system.talkgroups.find((_talkgroup: RadioTalkgroup) => _talkgroup.dec === +tg);
if (!talkgroup) { if (!talkgroup) {
delete this._avoids[sys][tg]; delete this.avoids[sys][tg];
} }
}); });
} else { } else {
delete this._avoids[sys]; delete this.avoids[sys];
} }
}); });
} }
private _unsubscribe(subscriptions: Subscription[] = [ private unsubscribe(subscriptions: Subscription[] = [
...this._subscriptions.liveFeed, ...this.subscriptions.liveFeed,
...this._subscriptions.systems, ...this.subscriptions.systems,
]) { ]) {
while (subscriptions.length) { while (subscriptions.length) {
subscriptions.pop().unsubscribe(); subscriptions.pop().unsubscribe();
} }
} }
private _unsubscribeLiveFeed(): void { private unsubscribeLiveFeed(): void {
this._unsubscribe(this._subscriptions.liveFeed); this.unsubscribe(this.subscriptions.liveFeed);
} }
private _writeAvoids(): void { private writeAvoids(): void {
const avoids: { [sys: number]: number[] } = {}; const avoids: { [sys: number]: number[] } = {};
Object.keys(this._avoids).map((sys: string) => +sys).forEach((sys: number) => { Object.keys(this.avoids).map((sys: string) => +sys).forEach((sys: number) => {
Object.keys(this._avoids[sys]).map((tg: string) => +tg).forEach((tg: number) => { Object.keys(this.avoids[sys]).map((tg: string) => +tg).forEach((tg: number) => {
if (this._avoids[sys][tg]) { if (this.avoids[sys][tg]) {
if (Array.isArray(avoids[sys])) { if (Array.isArray(avoids[sys])) {
avoids[sys].push(tg); avoids[sys].push(tg);
} else { } else {
@ -602,10 +605,10 @@ export class AppRadioService implements OnDestroy {
}); });
}); });
this._writeLocalStorage(LOCAL_STORAGE_KEY.avoids, avoids); this.writeLocalStorage(LOCAL_STORAGE_KEY.avoids, avoids);
} }
private _writeLocalStorage(key: string, value: any): void { private writeLocalStorage(key: string, value: any): void {
if (window instanceof Window && window.localStorage instanceof Storage) { if (window instanceof Window && window.localStorage instanceof Storage) {
window.localStorage.setItem(key, JSON.stringify(value)); window.localStorage.setItem(key, JSON.stringify(value));
} }