mirror of
https://github.com/chuot/rdio-scanner.git
synced 2026-08-13 08:33:00 -06:00
Major change from HTML5 audio to AudioContext; Apple products now works!
This commit is contained in:
parent
5bbc27853f
commit
8e4c09fde4
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue