@iobroker/youtube-lounge 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Denis Haev <dogafox@gmail.com>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,92 @@
1
- # Temporary Holding Version
1
+ # @iobroker/youtube-lounge
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Control the YouTube app of a TV over the YouTube Lounge API - the way "Link with TV" of the YouTube app on a phone
4
+ works: play a video, a playlist or the uploads of a channel, pause, skip, seek, and follow what the TV plays.
5
+
6
+ It works with every device whose YouTube app offers "Link with TV": smart TVs (Samsung, LG, Sony, Philips, ...),
7
+ Android TV / Google TV, Fire TV, game consoles.
8
+
9
+ **Note:** The Lounge API is not documented by Google; the protocol follows
10
+ [pyytlounge](https://github.com/FabioGNR/pyytlounge) and may change. The requests go to Google's servers
11
+ (`https://www.youtube.com/api/lounge`), so an internet connection is needed. Only finding the TV and reading its
12
+ screen ID (DIAL) stay in the local network.
13
+
14
+ ## Usage
15
+
16
+ ```js
17
+ const {
18
+ YouTubeLounge,
19
+ discoverDialDevices,
20
+ getYouTubeScreenId,
21
+ parseYouTubeTarget,
22
+ } = require('@iobroker/youtube-lounge');
23
+
24
+ const lounge = new YouTubeLounge({ name: 'ioBroker' });
25
+
26
+ // Pair: either with the screen ID the TV reports over DIAL ...
27
+ const [tv] = await discoverDialDevices();
28
+ const screenId = await getYouTubeScreenId(tv.appUrl); // many TVs report it only while the app runs
29
+ await lounge.pairWithScreenId(screenId);
30
+ // ... or with the code of the YouTube app on the TV: Settings > Link with TV code
31
+ await lounge.pairWithCode('123 456 789 012');
32
+
33
+ // lounge.screen can be stored and given to lounge.useScreen() later, so no pairing is needed again
34
+
35
+ await lounge.play(parseYouTubeTarget('https://www.youtube.com/watch?v=jNQXAC9IVRw'));
36
+ await lounge.play(parseYouTubeTarget('UCbmF2AHnpQsogOLW-MxBiJg')); // the uploads of a channel
37
+ await lounge.play(parseYouTubeTarget('pause'));
38
+ await lounge.seekTo(90);
39
+
40
+ lounge.on('nowPlaying', playing => console.log(playing)); // { videoId, listId, currentTime, duration, state } or null
41
+ lounge.on('volume', ({ volume, muted }) => console.log(volume, muted));
42
+ lounge.subscribe();
43
+ // ...
44
+ lounge.close();
45
+ ```
46
+
47
+ `parseYouTubeTarget` takes a video ID, a playlist ID (`PL…`), a channel ID (`UC…`), a youtube.com / youtu.be URL, or
48
+ `play`, `pause`, `next`, `previous`. Handles (`@name`) cannot be resolved without the YouTube Data API.
49
+
50
+ An expired lounge token is renewed with the screen ID, an ended session is opened again, both on their own.
51
+
52
+ Google accepts commands also while the YouTube app of the TV is not open, and they get lost. Right after the TV or
53
+ the app was started, wait for it: `await lounge.waitForScreen(30000)` (`true` once the app is there);
54
+ `lounge.screenOnline` and the event `screen` tell it later on.
55
+
56
+ <!--
57
+ Placeholder for the next version (at the beginning of the line):
58
+ ### **WORK IN PROGRESS**
59
+ -->
60
+
61
+ ## Changelog
62
+ ### 0.2.0 (2026-10-24)
63
+
64
+ - (@GermanBluefox) Added `waitForScreen()`, `screenOnline` and the event `screen`: commands sent before the YouTube app joined got lost
65
+
66
+ ### 0.1.0 (2026-10-24)
67
+
68
+ - (@GermanBluefox) Initial release, taken from the adapter `samsungtv`
69
+
70
+ ## License
71
+
72
+ MIT License
73
+
74
+ Copyright (c) 2026 Denis Haev <dogafox@gmail.com>
75
+
76
+ Permission is hereby granted, free of charge, to any person obtaining a copy
77
+ of this software and associated documentation files (the "Software"), to deal
78
+ in the Software without restriction, including without limitation the rights
79
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
80
+ copies of the Software, and to permit persons to whom the Software is
81
+ furnished to do so, subject to the following conditions:
82
+
83
+ The above copyright notice and this permission notice shall be included in all
84
+ copies or substantial portions of the Software.
85
+
86
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
87
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
88
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
89
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
90
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
91
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
92
+ SOFTWARE.
@@ -0,0 +1,14 @@
1
+ /** An event of the lounge channel: `[id, [type, ...args]]` */
2
+ export type LoungeEvent = [number, [string, ...unknown[]]];
3
+ /**
4
+ * Splits the chunked answer of bc/bind (`<length>\n<json>\n<length>\n<json>…`) into events. The length counts
5
+ * bytes, including the new line after the JSON. Data may arrive in pieces, as on the long poll; a chunk is
6
+ * reported once it is complete.
7
+ */
8
+ export declare class ChunkParser {
9
+ private buffer;
10
+ /** Add received data; returns the events of all chunks completed by it */
11
+ push(data: Buffer | Uint8Array | string): LoungeEvent[];
12
+ }
13
+ /** All events of a complete answer */
14
+ export declare function parseBindChunks(data: Buffer | string): LoungeEvent[];
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ChunkParser = void 0;
4
+ exports.parseBindChunks = parseBindChunks;
5
+ /**
6
+ * Splits the chunked answer of bc/bind (`<length>\n<json>\n<length>\n<json>…`) into events. The length counts
7
+ * bytes, including the new line after the JSON. Data may arrive in pieces, as on the long poll; a chunk is
8
+ * reported once it is complete.
9
+ */
10
+ class ChunkParser {
11
+ buffer = Buffer.alloc(0);
12
+ /** Add received data; returns the events of all chunks completed by it */
13
+ push(data) {
14
+ this.buffer = Buffer.concat([this.buffer, typeof data === 'string' ? Buffer.from(data, 'utf8') : data]);
15
+ const events = [];
16
+ for (;;) {
17
+ const newLine = this.buffer.indexOf(0x0a);
18
+ if (newLine < 0) {
19
+ break;
20
+ }
21
+ const header = this.buffer.subarray(0, newLine).toString('ascii').trim();
22
+ if (!/^\d+$/.test(header)) {
23
+ // not a length: skip the line
24
+ this.buffer = this.buffer.subarray(newLine + 1);
25
+ continue;
26
+ }
27
+ const length = Number(header);
28
+ const start = newLine + 1;
29
+ if (this.buffer.length < start + length) {
30
+ break;
31
+ }
32
+ const chunk = this.buffer.subarray(start, start + length).toString('utf8');
33
+ this.buffer = this.buffer.subarray(start + length);
34
+ try {
35
+ events.push(...JSON.parse(chunk));
36
+ }
37
+ catch {
38
+ // not JSON: ignored
39
+ }
40
+ }
41
+ return events;
42
+ }
43
+ }
44
+ exports.ChunkParser = ChunkParser;
45
+ /** All events of a complete answer */
46
+ function parseBindChunks(data) {
47
+ return new ChunkParser().push(data);
48
+ }
49
+ //# sourceMappingURL=chunks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chunks.js","sourceRoot":"","sources":["../src/chunks.ts"],"names":[],"mappings":";;;AA4CA,0CAEC;AA3CD;;;;GAIG;AACH,MAAa,WAAW;IACZ,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAEjC,0EAA0E;IAC1E,IAAI,CAAC,IAAkC;QACnC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,MAAM,EAAE,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;QACxG,MAAM,MAAM,GAAkB,EAAE,CAAC;QACjC,SAAS,CAAC;YACN,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;YAC1C,IAAI,OAAO,GAAG,CAAC,EAAE,CAAC;gBACd,MAAM;YACV,CAAC;YACD,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC;YACzE,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;gBACxB,8BAA8B;gBAC9B,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC;gBAChD,SAAS;YACb,CAAC;YACD,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;YAC9B,MAAM,KAAK,GAAG,OAAO,GAAG,CAAC,CAAC;YAC1B,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,KAAK,GAAG,MAAM,EAAE,CAAC;gBACtC,MAAM;YACV,CAAC;YACD,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,EAAE,KAAK,GAAG,MAAM,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;YAC3E,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,GAAG,MAAM,CAAC,CAAC;YACnD,IAAI,CAAC;gBACD,MAAM,CAAC,IAAI,CAAC,GAAI,IAAI,CAAC,KAAK,CAAC,KAAK,CAAmB,CAAC,CAAC;YACzD,CAAC;YAAC,MAAM,CAAC;gBACL,oBAAoB;YACxB,CAAC;QACL,CAAC;QACD,OAAO,MAAM,CAAC;IAClB,CAAC;CACJ;AAjCD,kCAiCC;AAED,sCAAsC;AACtC,SAAgB,eAAe,CAAC,IAAqB;IACjD,OAAO,IAAI,WAAW,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACxC,CAAC"}
@@ -0,0 +1,42 @@
1
+ /** A device with a DIAL server, e.g. a TV with the YouTube app */
2
+ export interface DialDevice {
3
+ /** Address of the device */
4
+ ip: string;
5
+ /** Name from the device description */
6
+ name: string;
7
+ manufacturer: string;
8
+ model: string;
9
+ /** UDN of the device description, a stable ID */
10
+ udn: string;
11
+ /** Base URL of the DIAL apps (`Application-URL`), e.g. `http://192.168.1.147:8080/ws/app/` */
12
+ appUrl: string;
13
+ }
14
+ /**
15
+ * Read the device description of a DIAL device.
16
+ *
17
+ * @param location the `LOCATION` of its SSDP answer
18
+ * @returns the device, `null` if it has no DIAL `Application-URL`
19
+ */
20
+ export declare function readDialDevice(location: string): Promise<DialDevice | null>;
21
+ /**
22
+ * Find DIAL devices with SSDP. The search goes out on every IPv4 interface: a host with several of them (docker,
23
+ * VPN) sends a single multicast out of one only.
24
+ *
25
+ * @param timeout how long to wait for answers, in ms
26
+ */
27
+ export declare function discoverDialDevices(timeout?: number): Promise<DialDevice[]>;
28
+ /**
29
+ * Read the screen ID of the YouTube app from its DIAL description. Many TVs report it only while the app runs or
30
+ * has run since they were switched on.
31
+ *
32
+ * @param appUrl the `appUrl` of the device
33
+ * @returns the screen ID, `null` if the app reports none
34
+ */
35
+ export declare function getYouTubeScreenId(appUrl: string): Promise<string | null>;
36
+ /**
37
+ * Start the YouTube app over DIAL. Some TVs refuse it (The Frame 2022: 403); then start it in another way.
38
+ *
39
+ * @param appUrl the `appUrl` of the device
40
+ * @returns whether the TV accepted it
41
+ */
42
+ export declare function launchYouTubeOverDial(appUrl: string): Promise<boolean>;
package/build/dial.js ADDED
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.readDialDevice = readDialDevice;
7
+ exports.discoverDialDevices = discoverDialDevices;
8
+ exports.getYouTubeScreenId = getYouTubeScreenId;
9
+ exports.launchYouTubeOverDial = launchYouTubeOverDial;
10
+ const node_dgram_1 = __importDefault(require("node:dgram"));
11
+ const node_http_1 = __importDefault(require("node:http"));
12
+ const node_os_1 = __importDefault(require("node:os"));
13
+ const SSDP_ADDRESS = '239.255.255.250';
14
+ const SSDP_PORT = 1900;
15
+ const DIAL_SERVICE = 'urn:dial-multiscreen-org:service:dial:1';
16
+ function request(url, method = 'GET', timeout = 5000) {
17
+ return new Promise(resolve => {
18
+ const req = node_http_1.default.request(url, { method, timeout, agent: false }, res => {
19
+ let body = '';
20
+ res.setEncoding('utf8');
21
+ res.on('data', (chunk) => (body += chunk));
22
+ res.on('end', () => resolve({ status: res.statusCode ?? 0, headers: res.headers, body }));
23
+ });
24
+ req.on('timeout', () => req.destroy());
25
+ req.on('error', () => resolve({ status: 0, headers: {}, body: '' }));
26
+ req.end();
27
+ });
28
+ }
29
+ function xmlValue(xml, tag) {
30
+ return new RegExp(`<${tag}>([^<]*)</${tag}>`).exec(xml)?.[1]?.trim() ?? '';
31
+ }
32
+ /**
33
+ * Read the device description of a DIAL device.
34
+ *
35
+ * @param location the `LOCATION` of its SSDP answer
36
+ * @returns the device, `null` if it has no DIAL `Application-URL`
37
+ */
38
+ async function readDialDevice(location) {
39
+ const { status, headers, body } = await request(location);
40
+ const appUrl = headers['application-url'];
41
+ if (status !== 200 || typeof appUrl !== 'string' || !appUrl) {
42
+ return null;
43
+ }
44
+ return {
45
+ ip: new URL(location).hostname,
46
+ name: xmlValue(body, 'friendlyName'),
47
+ manufacturer: xmlValue(body, 'manufacturer'),
48
+ model: xmlValue(body, 'modelName'),
49
+ udn: xmlValue(body, 'UDN'),
50
+ appUrl: appUrl.endsWith('/') ? appUrl : `${appUrl}/`,
51
+ };
52
+ }
53
+ /**
54
+ * Find DIAL devices with SSDP. The search goes out on every IPv4 interface: a host with several of them (docker,
55
+ * VPN) sends a single multicast out of one only.
56
+ *
57
+ * @param timeout how long to wait for answers, in ms
58
+ */
59
+ async function discoverDialDevices(timeout = 4000) {
60
+ const message = Buffer.from(`M-SEARCH * HTTP/1.1\r\nHOST: ${SSDP_ADDRESS}:${SSDP_PORT}\r\nMAN: "ssdp:discover"\r\nMX: 2\r\nST: ${DIAL_SERVICE}\r\n\r\n`);
61
+ const addresses = Object.values(node_os_1.default.networkInterfaces())
62
+ .flat()
63
+ .filter(entry => entry && entry.family === 'IPv4' && !entry.internal)
64
+ .map(entry => entry.address);
65
+ const locations = new Set();
66
+ await Promise.all((addresses.length ? addresses : ['0.0.0.0']).map(address => new Promise(resolve => {
67
+ const socket = node_dgram_1.default.createSocket({ type: 'udp4', reuseAddr: true });
68
+ socket.on('message', answer => {
69
+ const location = /^location:\s*(\S+)/im.exec(answer.toString('utf8'))?.[1];
70
+ if (location) {
71
+ locations.add(location);
72
+ }
73
+ });
74
+ socket.on('error', () => resolve());
75
+ socket.bind(0, address, () => {
76
+ try {
77
+ socket.setMulticastInterface(address);
78
+ }
79
+ catch {
80
+ // the default interface then
81
+ }
82
+ socket.send(message, SSDP_PORT, SSDP_ADDRESS);
83
+ setTimeout(() => {
84
+ socket.close();
85
+ resolve();
86
+ }, timeout);
87
+ });
88
+ })));
89
+ const devices = await Promise.all([...locations].map(location => readDialDevice(location)));
90
+ const byUdn = new Map();
91
+ for (const device of devices) {
92
+ if (device) {
93
+ byUdn.set(device.udn || device.ip, device);
94
+ }
95
+ }
96
+ return [...byUdn.values()];
97
+ }
98
+ /**
99
+ * Read the screen ID of the YouTube app from its DIAL description. Many TVs report it only while the app runs or
100
+ * has run since they were switched on.
101
+ *
102
+ * @param appUrl the `appUrl` of the device
103
+ * @returns the screen ID, `null` if the app reports none
104
+ */
105
+ async function getYouTubeScreenId(appUrl) {
106
+ const { status, body } = await request(`${appUrl}YouTube`);
107
+ return status === 200 ? xmlValue(body, 'screenId') || null : null;
108
+ }
109
+ /**
110
+ * Start the YouTube app over DIAL. Some TVs refuse it (The Frame 2022: 403); then start it in another way.
111
+ *
112
+ * @param appUrl the `appUrl` of the device
113
+ * @returns whether the TV accepted it
114
+ */
115
+ async function launchYouTubeOverDial(appUrl) {
116
+ const { status } = await request(`${appUrl}YouTube`, 'POST');
117
+ return status >= 200 && status < 300;
118
+ }
119
+ //# sourceMappingURL=dial.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dial.js","sourceRoot":"","sources":["../src/dial.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ /** An event of the lounge channel: `[id, [type, ...args]]` */
2
+ export type LoungeEvent = [number, [string, ...unknown[]]];
3
+ /**
4
+ * Splits the chunked answer of bc/bind (`<length>\n<json>\n<length>\n<json>…`) into events. The length counts
5
+ * bytes, including the new line after the JSON. Data may arrive in pieces, as on the long poll; a chunk is
6
+ * reported once it is complete.
7
+ */
8
+ export declare class ChunkParser {
9
+ private buffer;
10
+ /** Add received data; returns the events of all chunks completed by it */
11
+ push(data: Buffer | Uint8Array | string): LoungeEvent[];
12
+ }
13
+ /** All events of a complete answer */
14
+ export declare function parseBindChunks(data: Buffer | string): LoungeEvent[];
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Splits the chunked answer of bc/bind (`<length>\n<json>\n<length>\n<json>…`) into events. The length counts
3
+ * bytes, including the new line after the JSON. Data may arrive in pieces, as on the long poll; a chunk is
4
+ * reported once it is complete.
5
+ */
6
+ export class ChunkParser {
7
+ buffer = Buffer.alloc(0);
8
+ /** Add received data; returns the events of all chunks completed by it */
9
+ push(data) {
10
+ this.buffer = Buffer.concat([this.buffer, typeof data === 'string' ? Buffer.from(data, 'utf8') : data]);
11
+ const events = [];
12
+ for (;;) {
13
+ const newLine = this.buffer.indexOf(0x0a);
14
+ if (newLine < 0) {
15
+ break;
16
+ }
17
+ const header = this.buffer.subarray(0, newLine).toString('ascii').trim();
18
+ if (!/^\d+$/.test(header)) {
19
+ // not a length: skip the line
20
+ this.buffer = this.buffer.subarray(newLine + 1);
21
+ continue;
22
+ }
23
+ const length = Number(header);
24
+ const start = newLine + 1;
25
+ if (this.buffer.length < start + length) {
26
+ break;
27
+ }
28
+ const chunk = this.buffer.subarray(start, start + length).toString('utf8');
29
+ this.buffer = this.buffer.subarray(start + length);
30
+ try {
31
+ events.push(...JSON.parse(chunk));
32
+ }
33
+ catch {
34
+ // not JSON: ignored
35
+ }
36
+ }
37
+ return events;
38
+ }
39
+ }
40
+ /** All events of a complete answer */
41
+ export function parseBindChunks(data) {
42
+ return new ChunkParser().push(data);
43
+ }
44
+ //# sourceMappingURL=chunks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chunks.js","sourceRoot":"","sources":["../../src/chunks.ts"],"names":[],"mappings":"AAGA;;;;GAIG;AACH,MAAM,OAAO,WAAW;IACZ,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAEjC,0EAA0E;IAC1E,IAAI,CAAC,IAAkC;QACnC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,MAAM,EAAE,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;QACxG,MAAM,MAAM,GAAkB,EAAE,CAAC;QACjC,SAAS,CAAC;YACN,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;YAC1C,IAAI,OAAO,GAAG,CAAC,EAAE,CAAC;gBACd,MAAM;YACV,CAAC;YACD,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC;YACzE,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;gBACxB,8BAA8B;gBAC9B,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC;gBAChD,SAAS;YACb,CAAC;YACD,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;YAC9B,MAAM,KAAK,GAAG,OAAO,GAAG,CAAC,CAAC;YAC1B,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,KAAK,GAAG,MAAM,EAAE,CAAC;gBACtC,MAAM;YACV,CAAC;YACD,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,EAAE,KAAK,GAAG,MAAM,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;YAC3E,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,GAAG,MAAM,CAAC,CAAC;YACnD,IAAI,CAAC;gBACD,MAAM,CAAC,IAAI,CAAC,GAAI,IAAI,CAAC,KAAK,CAAC,KAAK,CAAmB,CAAC,CAAC;YACzD,CAAC;YAAC,MAAM,CAAC;gBACL,oBAAoB;YACxB,CAAC;QACL,CAAC;QACD,OAAO,MAAM,CAAC;IAClB,CAAC;CACJ;AAED,sCAAsC;AACtC,MAAM,UAAU,eAAe,CAAC,IAAqB;IACjD,OAAO,IAAI,WAAW,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACxC,CAAC"}
@@ -0,0 +1,42 @@
1
+ /** A device with a DIAL server, e.g. a TV with the YouTube app */
2
+ export interface DialDevice {
3
+ /** Address of the device */
4
+ ip: string;
5
+ /** Name from the device description */
6
+ name: string;
7
+ manufacturer: string;
8
+ model: string;
9
+ /** UDN of the device description, a stable ID */
10
+ udn: string;
11
+ /** Base URL of the DIAL apps (`Application-URL`), e.g. `http://192.168.1.147:8080/ws/app/` */
12
+ appUrl: string;
13
+ }
14
+ /**
15
+ * Read the device description of a DIAL device.
16
+ *
17
+ * @param location the `LOCATION` of its SSDP answer
18
+ * @returns the device, `null` if it has no DIAL `Application-URL`
19
+ */
20
+ export declare function readDialDevice(location: string): Promise<DialDevice | null>;
21
+ /**
22
+ * Find DIAL devices with SSDP. The search goes out on every IPv4 interface: a host with several of them (docker,
23
+ * VPN) sends a single multicast out of one only.
24
+ *
25
+ * @param timeout how long to wait for answers, in ms
26
+ */
27
+ export declare function discoverDialDevices(timeout?: number): Promise<DialDevice[]>;
28
+ /**
29
+ * Read the screen ID of the YouTube app from its DIAL description. Many TVs report it only while the app runs or
30
+ * has run since they were switched on.
31
+ *
32
+ * @param appUrl the `appUrl` of the device
33
+ * @returns the screen ID, `null` if the app reports none
34
+ */
35
+ export declare function getYouTubeScreenId(appUrl: string): Promise<string | null>;
36
+ /**
37
+ * Start the YouTube app over DIAL. Some TVs refuse it (The Frame 2022: 403); then start it in another way.
38
+ *
39
+ * @param appUrl the `appUrl` of the device
40
+ * @returns whether the TV accepted it
41
+ */
42
+ export declare function launchYouTubeOverDial(appUrl: string): Promise<boolean>;
@@ -0,0 +1,110 @@
1
+ import dgram from 'node:dgram';
2
+ import http from 'node:http';
3
+ import os from 'node:os';
4
+ const SSDP_ADDRESS = '239.255.255.250';
5
+ const SSDP_PORT = 1900;
6
+ const DIAL_SERVICE = 'urn:dial-multiscreen-org:service:dial:1';
7
+ function request(url, method = 'GET', timeout = 5000) {
8
+ return new Promise(resolve => {
9
+ const req = http.request(url, { method, timeout, agent: false }, res => {
10
+ let body = '';
11
+ res.setEncoding('utf8');
12
+ res.on('data', (chunk) => (body += chunk));
13
+ res.on('end', () => resolve({ status: res.statusCode ?? 0, headers: res.headers, body }));
14
+ });
15
+ req.on('timeout', () => req.destroy());
16
+ req.on('error', () => resolve({ status: 0, headers: {}, body: '' }));
17
+ req.end();
18
+ });
19
+ }
20
+ function xmlValue(xml, tag) {
21
+ return new RegExp(`<${tag}>([^<]*)</${tag}>`).exec(xml)?.[1]?.trim() ?? '';
22
+ }
23
+ /**
24
+ * Read the device description of a DIAL device.
25
+ *
26
+ * @param location the `LOCATION` of its SSDP answer
27
+ * @returns the device, `null` if it has no DIAL `Application-URL`
28
+ */
29
+ export async function readDialDevice(location) {
30
+ const { status, headers, body } = await request(location);
31
+ const appUrl = headers['application-url'];
32
+ if (status !== 200 || typeof appUrl !== 'string' || !appUrl) {
33
+ return null;
34
+ }
35
+ return {
36
+ ip: new URL(location).hostname,
37
+ name: xmlValue(body, 'friendlyName'),
38
+ manufacturer: xmlValue(body, 'manufacturer'),
39
+ model: xmlValue(body, 'modelName'),
40
+ udn: xmlValue(body, 'UDN'),
41
+ appUrl: appUrl.endsWith('/') ? appUrl : `${appUrl}/`,
42
+ };
43
+ }
44
+ /**
45
+ * Find DIAL devices with SSDP. The search goes out on every IPv4 interface: a host with several of them (docker,
46
+ * VPN) sends a single multicast out of one only.
47
+ *
48
+ * @param timeout how long to wait for answers, in ms
49
+ */
50
+ export async function discoverDialDevices(timeout = 4000) {
51
+ const message = Buffer.from(`M-SEARCH * HTTP/1.1\r\nHOST: ${SSDP_ADDRESS}:${SSDP_PORT}\r\nMAN: "ssdp:discover"\r\nMX: 2\r\nST: ${DIAL_SERVICE}\r\n\r\n`);
52
+ const addresses = Object.values(os.networkInterfaces())
53
+ .flat()
54
+ .filter(entry => entry && entry.family === 'IPv4' && !entry.internal)
55
+ .map(entry => entry.address);
56
+ const locations = new Set();
57
+ await Promise.all((addresses.length ? addresses : ['0.0.0.0']).map(address => new Promise(resolve => {
58
+ const socket = dgram.createSocket({ type: 'udp4', reuseAddr: true });
59
+ socket.on('message', answer => {
60
+ const location = /^location:\s*(\S+)/im.exec(answer.toString('utf8'))?.[1];
61
+ if (location) {
62
+ locations.add(location);
63
+ }
64
+ });
65
+ socket.on('error', () => resolve());
66
+ socket.bind(0, address, () => {
67
+ try {
68
+ socket.setMulticastInterface(address);
69
+ }
70
+ catch {
71
+ // the default interface then
72
+ }
73
+ socket.send(message, SSDP_PORT, SSDP_ADDRESS);
74
+ setTimeout(() => {
75
+ socket.close();
76
+ resolve();
77
+ }, timeout);
78
+ });
79
+ })));
80
+ const devices = await Promise.all([...locations].map(location => readDialDevice(location)));
81
+ const byUdn = new Map();
82
+ for (const device of devices) {
83
+ if (device) {
84
+ byUdn.set(device.udn || device.ip, device);
85
+ }
86
+ }
87
+ return [...byUdn.values()];
88
+ }
89
+ /**
90
+ * Read the screen ID of the YouTube app from its DIAL description. Many TVs report it only while the app runs or
91
+ * has run since they were switched on.
92
+ *
93
+ * @param appUrl the `appUrl` of the device
94
+ * @returns the screen ID, `null` if the app reports none
95
+ */
96
+ export async function getYouTubeScreenId(appUrl) {
97
+ const { status, body } = await request(`${appUrl}YouTube`);
98
+ return status === 200 ? xmlValue(body, 'screenId') || null : null;
99
+ }
100
+ /**
101
+ * Start the YouTube app over DIAL. Some TVs refuse it (The Frame 2022: 403); then start it in another way.
102
+ *
103
+ * @param appUrl the `appUrl` of the device
104
+ * @returns whether the TV accepted it
105
+ */
106
+ export async function launchYouTubeOverDial(appUrl) {
107
+ const { status } = await request(`${appUrl}YouTube`, 'POST');
108
+ return status >= 200 && status < 300;
109
+ }
110
+ //# sourceMappingURL=dial.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dial.js","sourceRoot":"","sources":["../../src/dial.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ export { ChunkParser, parseBindChunks, type LoungeEvent } from './chunks.js';
2
+ export { discoverDialDevices, getYouTubeScreenId, launchYouTubeOverDial, readDialDevice, type DialDevice, } from './dial.js';
3
+ export { PlayerState, YouTubeLounge, type LoungeOptions, type LoungeScreen, type NowPlaying } from './lounge.js';
4
+ export { parseYouTubeTarget, uploadsPlaylistId, type PlayerCommand, type YouTubeTarget } from './target.js';
@@ -0,0 +1,5 @@
1
+ export { ChunkParser, parseBindChunks } from './chunks.js';
2
+ export { discoverDialDevices, getYouTubeScreenId, launchYouTubeOverDial, readDialDevice, } from './dial.js';
3
+ export { PlayerState, YouTubeLounge } from './lounge.js';
4
+ export { parseYouTubeTarget, uploadsPlaylistId } from './target.js';
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,eAAe,EAAoB,MAAM,aAAa,CAAC;AAC7E,OAAO,EACH,mBAAmB,EACnB,kBAAkB,EAClB,qBAAqB,EACrB,cAAc,GAEjB,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,WAAW,EAAE,aAAa,EAA0D,MAAM,aAAa,CAAC;AACjH,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAA0C,MAAM,aAAa,CAAC"}