Skip to content

Commit 89e80b6

Browse files
committed
fix(chromecast): Debounce avahi discovery
1 parent 564295e commit 89e80b6

File tree

3 files changed

+108
-12
lines changed

3 files changed

+108
-12
lines changed

src/backend/sources/ChromecastSource.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ export class ChromecastSource extends MemorySource {
154154
await discoveryAvahi('_googlecast._tcp', {
155155
logger: this.logger,
156156
sanity: initial,
157-
onDiscover: (service, raw) => {
157+
onDiscover: (service) => {
158158
this.initializeDevice(service);
159159
},
160160
});
@@ -168,7 +168,7 @@ export class ChromecastSource extends MemorySource {
168168
await discoveryNative('_googlecast._tcp', {
169169
logger: this.logger,
170170
sanity: initial,
171-
onDiscover: (service, raw) => {
171+
onDiscover: (service) => {
172172
this.initializeDevice(service);
173173
},
174174
});

src/backend/utils/MDNSUtils.ts

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { sleep } from "../utils.js";
55
import {ErrorWithCause} from "pony-cause";
66
import { MdnsDeviceInfo } from "../common/infrastructure/Atomic.js";
77
import {Browser, Service, ServiceType} from "@astronautlabs/mdns";
8+
import {debounce, DebouncedFunction} from "./debounce.js";
89

910
export interface AvahiService {
1011
service_name: string
@@ -19,7 +20,7 @@ export interface AvahiService {
1920

2021
export interface DiscoveryOptions<T> {
2122
sanity?: boolean
22-
onDiscover?: (service: MdnsDeviceInfo, raw: T) => void
23+
onDiscover?: (service: MdnsDeviceInfo) => void
2324
onDnsError?: (err: Error) => void
2425
duration?: number,
2526
logger?: Logger
@@ -38,17 +39,42 @@ export const discoveryAvahi = async (service: string, options?: DiscoveryOptions
3839
maybeLogger.debug(`Starting mDNS discovery with Avahi => Listening for ${(duration / 1000).toFixed(2)}s`);
3940
let anyDiscovered = false;
4041

42+
let services = new Map<string, MdnsDeviceInfo>();
43+
44+
const triggerDiscovery = () => {
45+
for(const [k,v] of services.entries()) {
46+
maybeLogger.debug(`Discovered device "${v.name}" with ${v.addresses.length} interfaces - first host seen: ${v.addresses[0]}`);
47+
onDiscover(v);
48+
services.delete(k);
49+
}
50+
}
51+
52+
let debouncedFunc: DebouncedFunction;
53+
4154
try {
4255
const browser = new AvahiBrowser(service);
4356
browser.on(AvahiBrowser.EVENT_SERVICE_UP, async (service: AvahiService) => {
4457
anyDiscovered = true;
45-
maybeLogger.debug(`Discovered device "${service.service_name}" at ${service.target.host}`);
46-
if (onDiscover !== undefined) {
47-
onDiscover({
48-
name: service.service_name,
49-
addresses: [service.target.host],
50-
type: service.target.service_type
51-
}, service)
58+
59+
if(onDiscover !== undefined) {
60+
61+
let foundService = services.get(service.service_name);
62+
if(foundService === undefined) {
63+
foundService = {
64+
name: service.service_name,
65+
addresses: [service.target.host],
66+
type: service.target.service_type,
67+
}
68+
} else {
69+
foundService.addresses.push(service.target.host);
70+
}
71+
services.set(foundService.name, foundService);
72+
73+
if(debouncedFunc === undefined) {
74+
debouncedFunc = debounce(() => triggerDiscovery(), 1000);
75+
} else {
76+
await debouncedFunc();
77+
}
5278
}
5379
});
5480
browser.on(AvahiBrowser.EVENT_DNSSD_ERROR, (err) => {
@@ -109,9 +135,9 @@ export const discoveryNative = async (service: string, options?: DiscoveryOption
109135

110136
const browser = new Browser(service, {resolve: true})
111137
.on('serviceUp', async (service) => {
112-
maybeLogger.debug(`Discovered device "${service.name}" at ${service.addresses?.[0]}`);
138+
maybeLogger.debug(`Discovered device "${service.name}" with ${service.addresses.length} interfaces -- first host seen: ${service.addresses?.[0]}`);
113139
if (onDiscover) {
114-
onDiscover({name: service.name, addresses: service.addresses, type: service.service_type}, service);
140+
onDiscover({name: service.name, addresses: service.addresses, type: service.service_type});
115141
}
116142
})
117143
browser.on('error', (err) => {

src/backend/utils/debounce.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// ES6 Async version of the "classic" JavaScript Debounce function.
2+
// Works both with and without promises, so you can replace your existing
3+
// debounce helper function with this one (and it will behave the same).
4+
// The only difference is that this one returns a promise, so you can use
5+
// it with async/await.
6+
//
7+
// I've converted this into a TypeScript module, and added a few more
8+
// features to it, such as the ability to cancel the debounce, and also
9+
// execute the function immediately, using the `doImmediately` method.
10+
//
11+
// Returns a function, that, as long as it continues to be invoked, will not
12+
// be triggered. The function will be called after it stops being called for
13+
// N milliseconds. If `immediate` is passed, trigger the function on the
14+
// leading edge, instead of the trailing.
15+
//
16+
// @author: @carlhannes
17+
// @param {Function} func - The function to debounce.
18+
// @param {Number} wait - The number of milliseconds to delay.
19+
// @param {Boolean} immediate - Whether to execute the function at the beginning.
20+
// @returns {Function} - The debounced function.
21+
// @example
22+
// import debounce from 'utils/debounce';
23+
//
24+
// const debounced = debounce(() => {
25+
// console.log('Hello world!');
26+
// }, 1000);
27+
//
28+
// debounced();
29+
//
30+
// https://gist.github.com/carlhannes/4b318c28e95f635191bffb656b9a2cfe
31+
export interface DebounceConstructor {
32+
(func: () => void, wait: number, immediate?: boolean): DebouncedFunction;
33+
}
34+
35+
export interface DebouncedFunction {
36+
(...args: unknown[]): Promise<unknown>;
37+
cancel(): void;
38+
doImmediately(...args: unknown[]): Promise<unknown>;
39+
}
40+
41+
export const debounce: DebounceConstructor = (func: () => void, wait: number, immediate?: boolean) => {
42+
let timeout: NodeJS.Timeout | null = null;
43+
const debouncedFn: DebouncedFunction = (...args) => new Promise((resolve) => {
44+
clearTimeout(timeout);
45+
timeout = setTimeout(() => {
46+
timeout = null;
47+
if (!immediate) {
48+
void Promise.resolve(func.apply(this, [...args])).then(resolve);
49+
}
50+
}, wait);
51+
if (immediate && !timeout) {
52+
void Promise.resolve(func.apply(this, [...args])).then(resolve);
53+
}
54+
});
55+
56+
debouncedFn.cancel = () => {
57+
clearTimeout(timeout);
58+
timeout = null;
59+
};
60+
61+
debouncedFn.doImmediately = (...args) => new Promise((resolve) => {
62+
clearTimeout(timeout);
63+
timeout = setTimeout(() => {
64+
timeout = null;
65+
void Promise.resolve(func.apply(this, [...args])).then(resolve);
66+
}, 0);
67+
});
68+
69+
return debouncedFn;
70+
};

0 commit comments

Comments
 (0)