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.

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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