{"id":16557658,"url":"https://github.com/patrickkfkan/yt-cast-receiver","last_synced_at":"2025-06-26T10:32:03.393Z","repository":{"id":43509743,"uuid":"357648719","full_name":"patrickkfkan/yt-cast-receiver","owner":"patrickkfkan","description":"YouTube Cast Receiver framework for Node.js","archived":false,"fork":false,"pushed_at":"2025-05-21T06:47:34.000Z","size":4927,"stargazers_count":39,"open_issues_count":2,"forks_count":5,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-06-22T04:40:54.088Z","etag":null,"topics":["cast","receiver","youtube","youtube-music"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/patrickkfkan.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null},"funding":{"ko_fi":"patrickkfkan"}},"created_at":"2021-04-13T18:15:10.000Z","updated_at":"2025-06-09T21:44:14.000Z","dependencies_parsed_at":"2024-10-29T07:45:12.618Z","dependency_job_id":"a7692948-040e-4c81-a7d8-3e2efef3e43c","html_url":"https://github.com/patrickkfkan/yt-cast-receiver","commit_stats":{"total_commits":9,"total_committers":1,"mean_commits":9.0,"dds":0.0,"last_synced_commit":"f228b5e3c06bef233fcca00bbaaf01003852d4ed"},"previous_names":[],"tags_count":11,"template":false,"template_full_name":null,"purl":"pkg:github/patrickkfkan/yt-cast-receiver","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/patrickkfkan%2Fyt-cast-receiver","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/patrickkfkan%2Fyt-cast-receiver/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/patrickkfkan%2Fyt-cast-receiver/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/patrickkfkan%2Fyt-cast-receiver/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/patrickkfkan","download_url":"https://codeload.github.com/patrickkfkan/yt-cast-receiver/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/patrickkfkan%2Fyt-cast-receiver/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":262047895,"owners_count":23250443,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cast","receiver","youtube","youtube-music"],"created_at":"2024-10-11T20:08:10.189Z","updated_at":"2025-06-26T10:32:03.386Z","avatar_url":"https://github.com/patrickkfkan.png","language":"TypeScript","funding_links":["https://ko-fi.com/patrickkfkan","https://ko-fi.com/C0C5RGOOP'"],"categories":[],"sub_categories":[],"readme":"\u003ca href='https://ko-fi.com/C0C5RGOOP' target='_blank'\u003e\u003cimg height='36' style='border:0px;height:36px;' src='https://storage.ko-fi.com/cdn/kofi2.png?v=3' border='0' alt='Buy Me a Coffee at ko-fi.com' /\u003e\u003c/a\u003e\n\n# yt-cast-receiver\n\nA YouTube Cast Receiver for Node that also supports casting from YouTube Music. Originally inspired by [TubeCast](https://github.com/enen92/script.tubecast).\n\n### Terminology\n\n**Sender**\n\nAn app that initiates a Cast session and acts as a remote controller for playback of content on the *receiver*. In the context of YouTube casting, a sender can be the YouTube mobile app or the YouTube website. Likewise, for YouTube Music, the sender can be the YouTube Music app or website.\n\n\u003e Not all browsers support casting from the YouTube (Music) website. This module has been tested to work with the Chrome and Edge desktop browsers.\n\n**Receiver**\n\nAn app that receives and responds to commands from a sender, plays content through a *player*, and provides status updates to senders.\n\n---\n\n`yt-cast-receiver` plays the role of the receiver in the context of YouTube casting, but does not include a player. It is intended to be integrated into your application where you implement the player yourself. When the receiver receives a playback command from a sender (such as play, pause and seek), it will pass this command to your player. It is up to you to decide how you would handle these commands.\n\nThe module includes a *DIAL server* for broadcasting the receiver device on the network. Users may then initiate a Cast session with the receiver through the sender app's Cast button.\n\n*This project is (always) work-in-progress and may not be reliable enough for production use.*\n\n# Installation\n\n```\nnpm i yt-cast-receiver --save\n```\n\n# Usage\n\n## Basic Usage\n\n### 1. Implement player\n\nFirst, implement your player by extending the `Player` class:\n```\n// ESM\nimport { Player } from 'yt-cast-receiver';\n// or CJS\nconst { Player } = require('yt-cast-receiver');\n\nclass MyPlayer extends Player {\n  ...\n}\n```\n\nThe methods you need to implement are:\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003edoPlay(video, position): Promise\u0026lt;boolean\u0026gt;\u003c/code\u003e\u003c/summary\u003e\n\u003cbr /\u003e\n\u003cp\u003eImplementations shall play the target video from the specified position.\u003c/p\u003e\n\n**Params**\n- `video`: (object) target video to play; [object properties](#videos).\n- `position`: (number) the position, in seconds, from which to start playback.\n\n**Returns**\n\u003cbr /\u003e\n\u003cp\u003ePromise that resolves to `true` on successful playback; `false` otherwise.\u003c/p\u003e\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003edoPause(): Promise\u0026lt;boolean\u0026gt;\u003c/code\u003e\u003c/summary\u003e\n\u003cbr /\u003e\n\u003cp\u003eImplementations shall pause current playback.\u003c/p\u003e\n\n**Returns**\n\u003cbr /\u003e\n\u003cp\u003ePromise that resolves to `true` when playback was paused; `false` otherwise.\u003c/p\u003e\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003edoResume(): Promise\u0026lt;boolean\u0026gt;\u003c/code\u003e\u003c/summary\u003e\n\u003cbr /\u003e\n\u003cp\u003eImplementations shall resume paused playback.\u003c/p\u003e\n\n**Returns**\n\u003cbr /\u003e\n\u003cp\u003ePromise that resolves to `true` when playback was resumed; `false` otherwise.\u003c/p\u003e\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003edoStop(): Promise\u0026lt;boolean\u0026gt;\u003c/code\u003e\u003c/summary\u003e\n\u003cbr /\u003e\n\u003cp\u003eImplementations shall stop current playback or cancel any pending playback (such as when a video is still being loaded).\u003c/p\u003e\n\n**Returns**\n\u003cbr /\u003e\n\u003cp\u003ePromise that resolves to `true` when playback was stopped or pending playback was cancelled; `false` otherwise.\u003c/p\u003e\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003edoSeek(position): Promise\u0026lt;boolean\u0026gt;\u003c/code\u003e\u003c/summary\u003e\n\u003cbr /\u003e\n\u003cp\u003eImplementations shall seek to the specified position.\u003cp\u003e\n\n**Params**\n- `position`: (number) the position, in seconds, to seek to.\n\n**Returns**\n\u003cbr /\u003e\n\u003cp\u003ePromise that resolves to `true` if seek operation was successful; `false` otherwise.\u003c/p\u003e\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003edoSetVolume(volume): Promise\u0026lt;boolean\u0026gt;\u003c/code\u003e\u003c/summary\u003e\n\u003cbr /\u003e\n\u003cp\u003eImplementations shall set the volume level and muted state to the values specified in the \u003ccode\u003evolume\u003c/code\u003e object param.\u003c/p\u003e\n\n**Params**\n- `volume`: (object)\n  - `level`: (number) volume level between 0-100.\n  - `muted`: (boolean) muted state.\n\n**Returns**\n\u003cbr /\u003e\n\u003cp\u003ePromise that resolves to `true` when volume was set; `false` otherwise.\u003c/p\u003e\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003edoGetVolume(): Promise\u0026lt;\u003ccode\u003eVolume\u003c/code\u003e\u0026gt;\u003c/code\u003e\u003c/summary\u003e\n\u003cbr /\u003e\n\u003cp\u003eImplementations shall return the current volume level and muted state.\u003c/p\u003e\n\n**Returns**\n\u003cp\u003e\n\nPromise that resolves to an object that satifies the [Volume](./docs/api/interfaces/Volume.md) interface constraint:\n\n- `level`: (number) volume level between 0-100.\n- `muted`: (boolean) muted state.\n\u003c/p\u003e\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003edoGetPosition(): Promise\u0026lt;number\u0026gt;\u003c/code\u003e\u003c/summary\u003e\n\u003cbr /\u003e\n\u003cp\u003eImplementations shall return the current playback position.\u003c/p\u003e\n\n**Returns**\n\u003cp\u003ePromise that resolves to the current playback position (in seconds).\u003c/p\u003e\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003edoGetDuration(): Promise\u0026lt;number\u0026gt;\u003c/code\u003e\u003c/summary\u003e\n\u003cbr /\u003e\n\u003cp\u003eImplementations shall return the duration of the current video.\u003c/p\u003e\n\n**Returns**\n\u003cp\u003ePromise that resolves to the duration of the current video (in seconds).\u003c/p\u003e\n\u003c/details\u003e\n\n### 2. Create receiver instance\n\nOnce you have implemented your player, you create a `YouTubeCastReceiver` instance with it:\n\n```\n// ESM\nimport YouTubeCastReceiver from 'yt-cast-receiver';\n// or CJS\nconst YouTubeCastReceiver = require('yt-cast-receiver');\n\n// Your player implementation\nconst player = new MyPlayer();\n\n// Create receiver instance with your player\nconst receiver = new YouTubeCastReceiver(player, [options]);\n```\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003eYouTubeCastReceiver\u003c/code\u003e options\u003c/summary\u003e\n\u003cbr /\u003e\n\nYou can configure the `YouTubeCastReceiver` instance by passing options to its constructor. All options are optional.\n\n(Object)\n- `app`: (object) receiver app options\n  - `playlistRequestHandler`: `PlaylistRequestHandler` implementation (default: `DefaultPlaylistRequestHandler` instance) - see [Player Queue](#player-queue).\n  - `enableAutoplayOnConnect`: (boolean) whether to enable autoplay on sender app when it connects (default: `true`).\n  - `mutePolicy`: (string) one of [Constants.MUTE_POLICIES](#constants) (default: AUTO). See [Mute Policy](#mute-policy).\n  - `resetPlayerOnDisconnectPolicy`: (string) one of [Constants.RESET_PLAYER_ON_DISCONNECT_POLICIES](#constants) (default: ALL_DISCONNECTED). See [Reset Player On Disconnect Policy](#reset-player-on-disconnect-policy).\n  - `screenApp`: (string) defaults to 'ytcr'.\n- `dial`: (object) DIAL server options\n  - `bindToAddresses`: Array\u003c`string`\u003e (default: `undefined` - bind to all network addresses).\n  - `bindToInterfaces`: Array\u003c`string`\u003e (default: `undefined` - bind to all network interfaces).\n  - `corsAllowOrigins`: (boolean) defaults to `false` - no origin allowed.\n  - `port`: (number) port on which to accept requests (default: 3000).\n  - `prefix`: (string) access path (default: '/ytcr').\n- `device`: (object) Info about the receiver device\n  - `name`: (string) name shown in a sender app's Cast menu, when the receiver device is discovered through DIAL. Defaults to hostname of the receiver device.\n  - `screenName`: (string) name shown in a sender app's Cast menu, when the receiver device was previously connected to through [manual pairing](#manual-pairing). Defaults to 'YouTube on `device.name`'.\n  - `brand`: (string) defaults to 'Generic'.\n  - `model`: (string) defaults to 'SmartTV'.\n- `dataStore`: `DataStore` implementation (default: `DefaultDataStore` instance) - see [DataStore](#datastore).\n- `logger`: `Logger` implementation (default: `DefaultLogger` instance) - see [Logging](#logging).\n- `logLevel`: one of [Constants.LOG_LEVELS](#constants) (default: INFO).\n\u003c/details\u003e\n\n### 3. Start receiver\n\nOnce you have created a receiver instance with your player implementation, you can register event listeners and begin accepting Cast requests from senders:\n\n```\n// When a sender connects\nreceiver.on('senderConnect', (sender) =\u003e {\n  console.log(`Connected to ${sender.name}`);\n});\n\n// When a sender disconnects\nreceiver.on('senderDisconnect', (sender, implicit) =\u003e {\n  console.log(`Disconnected from ${sender.name}.`);\n\n  // `yt-cast-receiver` supports multiple sender connections. Call\n  // `getConnectedSenders()` to obtain info about them.\n  console.log(`Remaining connected senders: ${receiver.getConnectedSenders().length}`);\n});\n\n// Start the receiver\ntry {\n  await receiver.start();\n}\ncatch (error) {\n  ...\n}\n```\n\n\u003e In `senderDisconnect` event, the `implicit` flag indicates whether the sender was disconnected due to external event, as opposed to explicit command by the user or receiver. For example, if the user taps the 'Stop Casting' button in the sender app, the `implicit` flag will be `false`. On the other hand, if the sender device disconnects because it has gone outside the range of the receiver's network, then `implicit` will be `true`.\n\nIf all goes well, you should see the receiver device listed among the discovered devices when you hit the Cast button in a sender app (the YouTube mobile app, for instance). Selecting it will initiate a Cast session with the receiver. Once connected, you can begin casting videos from the sender app.\n\n### 4. Stopping the receiver\n\n```\ntry {\n  await receiver.stop();\n}\ncatch (error) {\n  ...\n}\n```\n\n## Controls\n\nA user interacts with controls provided by the sender app to manage content played on the receiver or to set its volume. Such controls include:\n\n- Pause / previous / next buttons;\n- Volume slider or, in the case of phones, the physical volume up / down buttons;\n- Progress bar for seeking; and\n- Play button or, in the case of a YouTube sender app, the video thumbnails that start playback when tapped or clicked.\n\nWhen a user interacts with a control, the sender app sends the corresponding command to the receiver. The receiver responds by calling one of the following 'control' methods defined in the `Player` class:\n\n\u003ca name=\"control-methods\"\u003e\u003c/a\u003e\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003eplay(video, position, AID): Promise\u0026lt;boolean\u0026gt;\u003c/code\u003e\u003c/summary\u003e\n\u003cbr /\u003e\n\u003cp\u003e\nNotifies senders that player is in 'loading' state, then calls \u003ccode\u003edoPlay()\u003c/code\u003e; if returned Promise resolves to \u003ccode\u003etrue\u003c/code\u003e, notifies senders that playback has started.\n\u003c/p\u003e\n\n**Params**\n- `video`: (object) target video to play; [object properties](#videos).\n- `position`: (number) the position, in seconds, from which to start playback.\n- `AID`: internal use; do not specify.\n\n**Returns**\n\nPromise that resolves to the resolved result of `doPlay()`.\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003epause(AID): Promise\u0026lt;boolean\u0026gt;\u003c/code\u003e\u003c/summary\u003e\n\u003cbr /\u003e\n\u003cp\u003e\nCalls \u003ccode\u003edoPause()\u003c/code\u003e; if returned Promise resolves to \u003ccode\u003etrue\u003c/code\u003e, notifies connected senders that playback has paused.\n\u003c/p\u003e\n\n**Params**\n- `AID`: internal use; do not specify.\n\n**Returns**\n\nPromise that resolves to the resolved result of `doPause()`, or `false` if no playback is in progress.\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003eresume(AID): Promise\u0026lt;boolean\u0026gt;\u003c/code\u003e\u003c/summary\u003e\n\u003cbr /\u003e\n\u003cp\u003e\nCalls \u003ccode\u003edoResume()\u003c/code\u003e; if returned Promise resolves to \u003ccode\u003etrue\u003c/code\u003e, notifies connected senders that playback has resumed.\n\u003c/p\u003e\n\n**Params**\n- `AID`: internal use; do not specify.\n\n**Returns**\n\nPromise that resolves to the resolved result of `doResume()`, or `false` if player is not in paused state.\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003estop(AID): Promise\u0026lt;boolean\u0026gt;\u003c/code\u003e\u003c/summary\u003e\n\u003cbr /\u003e\n\u003cp\u003e\nCalls \u003ccode\u003edoStop()\u003c/code\u003e; if returned Promise resolves to \u003ccode\u003etrue\u003c/code\u003e, notifies connected senders that playback has stopped.\n\u003c/p\u003e\n\n**Params**\n- `AID`: internal use; do not specify.\n\n**Returns**\n\nPromise that resolves to the resolved result of `doStop()`, or `true` if player already in stopped or idle state.\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003eseek(position, AID): Promise\u0026lt;boolean\u0026gt;\u003c/code\u003e\u003c/summary\u003e\n\u003cbr /\u003e\n\u003cp\u003e\nCalls \u003ccode\u003edoSeek()\u003c/code\u003e; if returned Promise resolves to \u003ccode\u003etrue\u003c/code\u003e, notifies connected senders of new seek position.\n\u003c/p\u003e\n\n**Params**\n- `position`: (number) the position, in seconds, to seek to.\n- `AID`: internal use; do not specify.\n\n**Returns**\n\nPromise that resolves to the resolved result of `doSeek()`, or `false` if no playback is in progress or otherwise not in paused state.\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003enext(AID): Promise\u0026lt;boolean\u0026gt;\u003c/code\u003e\u003c/summary\u003e\n\u003cbr /\u003e\n\u003cp\u003e\nPlays the next video in the player queue. If already reached end of queue and autoplay is enabled, play autoplay video if available. Notifies senders on successful playback.\n\u003c/p\u003e\n\n**Params**\n- `AID`: internal use; do not specify.\n\n**Returns**\n\nPromise that resolves to `true` on playback of the next video; `false` otherwise.\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003eprevious(AID): Promise\u0026lt;boolean\u0026gt;\u003c/code\u003e\u003c/summary\u003e\n\u003cbr /\u003e\n\u003cp\u003e\nPlays the previous video in the player queue. Notifies senders on successful playback.\n\u003c/p\u003e\n\n**Params**\n- `AID`: internal use; do not specify.\n\n**Returns**\n\nPromise that resolves to `true` on playback of the previous video; `false` otherwise.\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003esetVolume(volume, AID): Promise\u0026lt;boolean\u0026gt;\u003c/code\u003e\u003c/summary\u003e\n\u003cbr /\u003e\n\u003cp\u003e\nCalls \u003ccode\u003edoSetVolume()\u003c/code\u003e; if returned Promise resolves to \u003ccode\u003etrue\u003c/code\u003e, notifies connected senders of new volume.\n\u003c/p\u003e\n\n**Params**\n- `volume`: (object)\n  - `level`: (number) volume level between 0-100.\n  - `muted`: (boolean) muted state.\n- `AID`: internal use; do not specify.\n\n**Returns**\n\nPromise that resolves to the resolved result of `doSetVolume()`.\n\u003c/details\u003e\n\nThese methods wrap around the `do***()` methods you implemented in your player and sends status updates back to the sender as necessary. The status updates enable the sender app to refresh its UI to match the state of the player.\n\nIf your application also provides controls for the user to manage playback on the receiver, you should call these methods to ensure senders will be notified of the relevant changes in player state. For example:\n\n```\n// Example: user clicks 'pause' button in your application's UI\nawait player.pause();\n```\n\n## Videos\n\nA video is represented by an object that satisfies the [Video](./docs/api/interfaces/Video.md) interface constraint:\n\n- `id`: (string) video Id.\n- `client`: (object) the client that is requesting playback; one of [Constants.CLIENTS](#constants).\n- `context`: (object)\n  - `playlistId`: (string) Id of the playlist containing the video.\n  - `params`: (string) Exact purpose remains to be ascertained, but appears to affect playlist content and autoplay video.\n  - `ctt`: (string) Client credentials transfer token, which is an authorization token for accessing private videos.\n\nAll properties of `context`, as well as `context` itself, are present only if available. Where provided, you may use them to fetch video info in your player implementation or, where applicable, your own `PlaylistRequestHandler` implementation (see [Player Queue](#player-queue)).\n\nFor example of fetching video info for purpose of playback, see [VideoLoader](./example/VideoLoader.ts) used by the [FakePlayer](./example/FakePlayer.ts) demo.\n\nFor example of fetching the next and previous videos in the [player queue](#player-queue), including autoplay video, see [DefaultPlaylistRequestHandler](./src/lib/app/DefaultPlaylistRequestHandler.ts).\n\n\u003e It is up to you to decide which properties to utilize in your implementation. For example, in your player implementation, you may just use `video.id` when fetching video streams for playback. On the other hand, if your player needs to be able to play private videos, then you must also utilize `video.context.ctt`.\n\n\n## Handling changes in player state\n\nIf a change in player state is effected by one of the previously mentioned ['control' methods](#control-methods), then you don't have to do anything as those methods will send status updates to senders.\n\nOn the other hand, if a change is one that only your player implementation knows about, then it is your responsibility to deal with it. Situations where this will arise are:\n\n#### 1. When playback finishes\n\nThe receiver has no knowledge when a playback reaches the end of a video. It is your responsibility to take action when this happens. Usually, this would be playing the next video in the queue:\n\n```\n// When playback finishes\nawait player.pause(); // Pause the player\nawait player.next();  // Play next video in the queue\n```\n\n#### 2. When your player implementation depends on an external player\n\nIf your player implementation delegates its functions to an external player, and the user is able to interact directly with that player, then you must be able to capture changes in the state of the external player arising from such interactions. You must then call the following method in your player implementation:\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003enotifyExternalStateChange(newStatus): Promise\u0026ltvoid\u0026gt\u003c/code\u003e\u003c/summary\u003e\n\u003cbr /\u003e\n\u003cp\u003e\nSignals that there has been a change in player state that is not captured elsewhere in the `Player` implementation. This method will update the `Player` instance's internal state and, if necessary, notifies senders of the new player state.\n\u003c/p\u003e\n\n**Params**\n- `newStatus`: the new player status (one of [Constants.PLAYER_STATUSES](#constants)); `undefined` for no change in player status.\n\u003c/details\u003e\n\nExample:\n```\n// Playback paused by external player\nplayer.notifyExternalStateChange(PLAYER_STATUSES.PAUSED);\n\n// Change in volume of external player\n// -- Player status has not changed, so we do not specify `newStatus`.\nplayer.notifyExternalStateChange();\n```\n\n## Manual Pairing\n\nManual pairing, aka 'Link with TV Code', allows a sender to connect to the receiver even when they are not on the same network. For this, you need to obtain a pairing code for the user to enter into the sender app.\n\n\u003e YouTube Music does not support this feature.\n\nA pairing code refreshes every five minutes. Thus, the receiver provides a `PairingCodeRequestService` for obtaining the pairing code and automatically refreshing it at five-minute intervals:\n\n```\n// Obtain `PairingCodeRequestService` instance from receiver\nconst service = receiver.getPairingCodeRequestService();\n\n// Events\nservice.on('request', () =\u003e {...});  // Event when request is being made\nservice.on('response', (code) =\u003e {...}); // Event when code is obtained\nservice.on('error', (error) =\u003e {...}); // Event when error occurs\n\n// Note that service stops on `error` event.\n\nservice.start();\n...\n\nservice.stop();\n```\nIn your application, you would inform the user of the pairing code inside the `response` event.\n\n\u003ca name=\"player-queue\"\u003e\u003c/a\u003e\n## Player Queue\n\nThe player queue is represented by a `Playlist` instance, accessible by `player.queue`. Internally, for the current selected video, the playlist keeps a reference to the previous and next videos. When the current video changes, the playlist refreshes the previous and next videos by calling the following method of a `PlaylistRequestHandler` implementation:\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003egetPreviousNextVideos(target, playlist)\u003c/code\u003e\u003c/summary\u003e\n\u003cbr /\u003e\n\u003cp\u003eGiven \u003ccode\u003etarget\u003c/code\u003e video that resides in \u003ccode\u003eplaylist\u003c/code\u003e, implementations shall fetch the previous and next videos in the list.\u003c/p\u003e\n\n**Params**\n- `video`: (object) target video for which to fetch the previous and next videos; [object properties](#videos).\n- `playlist`: (object) the `Playlist` instance making the request.\n\n**Returns**\n\u003cbr /\u003e\n\u003cp\u003e\nPromise that resolves to the following value:\n\n(Object)\n- `previous`: (object) the previous video in the list, or `null` if none.\n- `next`: (object) the next video in the list. If there is none, then set it to the autoplay video, or `null` if none or autoplay is not enabled (you may check with `playlist.autoplayMode`).\n\nInstead of `null`, you may just omit `previous` or `next` (as appropriate) from the returned result. If provided, they must satisfy the `Video` interface constraint (see [Videos](#videos)).\n\u003c/p\u003e\n\u003c/details\u003e\n\nBy default, previous / next videos are obtained through a `DefaultPlaylistRequestHandler` instance, which makes YouTube API calls with the [YouTube.js](https://github.com/LuanRT/YouTube.js) library and returns the result after parsing the response.\n\nYou may have observed that the player queue can be obtained through the `Player` instance's `queue` property, and that the queue provides a `videoIds` property:\n\n```\nconst videoIds = player.queue.videoIds; // Array of video Ids in the queue\n```\n\nSo why does `DefaultPlaylistRequestHandler` go through the trouble of making YouTube API calls when the Id of the previous and next videos are readily available? This is because the `videoIds` property does not include context data such as `params` and `ctt`, nor does it include information about autoplay. The aim of `DefaultPlaylistRequestHandler` is to facilitate autoplay as well as provide sufficient data that would allow you to properly formulate requests when fetching video info for playback in your player implementation.\n\nIf the default `PlaylistRequestHandler` does not suit your purpose, you can implement your own:\n```\nimport { Playlist, PlaylistRequestHandler, Video } from \"yt-cast-receiver\";\n\nclass MyPlaylistRequestHandler extends PlaylistRequestHandler {\n  getPreviousNextVideos(target: Video, playlist: Playlist): Promise\u003c{ previous?: Video | null; next?: Video | null; }\u003e {\n    // Do you stuff here\n  }\n}\n```\n\nTo use your `PlaylistRequestHandler` implementation:\n```\nconst receiver = new YouTubeCastReceiver(player, {\n  app: {\n    playlistRequestHandler: new MyPlaylistRequestHandler()\n    ...\n  }\n});\n```\n\n### Events\n\nTo capture events emitted by the player queue:\n\n```\nplayer.queue.on(eventType, (event) =\u003e { ... });\n```\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003eon('videoSelected', (event) =\u003e { ... });\u003c/code\u003e\u003c/summary\u003e\n\u003cbr /\u003e\n\u003cp\u003e\nEmitted when a video in the queue has been selected.\n\u003c/p\u003e\n\n`event`: (object)\n- `type`: (string) 'videoSelected'\n- `user`: (object) where applicable, the user that triggered the event.\n  - `name`: (string) name of the user\n  - `thumbnail`: (string) user avatar URI\n- `videoId`: (string) the Id of the video selected.\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003eon('videoAdded', (event) =\u003e { ... });\u003c/code\u003e\u003c/summary\u003e\n\u003cbr /\u003e\n\u003cp\u003e\nEmitted when a video has been added to the queue.\n\u003c/p\u003e\n\n`event`: (object)\n- `type`: (string) 'videoAdded'\n- `user`: (object) where applicable, the user that triggered the event.\n  - `name`: (string) name of the user\n  - `thumbnail`: (string) user avatar URI\n- `videoId`: (string) the Id of the video added.\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003eon('videoRemoved', (event) =\u003e { ... });\u003c/code\u003e\u003c/summary\u003e\n\u003cbr /\u003e\n\u003cp\u003e\nEmitted when a video in the queue has been removed.\n\u003c/p\u003e\n\n`event`: (object)\n- `type`: (string) 'videoRemoved'\n- `user`: (object) where applicable, the user that triggered the event.\n  - `name`: (string) name of the user\n  - `thumbnail`: (string) user avatar URI\n- `videoId`: (string) the Id of the video removed.\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003eon('playlistSet', (event) =\u003e { ... });\u003c/code\u003e\u003c/summary\u003e\n\u003cbr /\u003e\n\u003cp\u003e\nEmitted when the entire content of the queue has changed.\n\u003c/p\u003e\n\n`event`: (object)\n- `type`: (string) 'playlistSet'\n- `user`: (object) where applicable, the user that triggered the event.\n  - `name`: (string) name of the user\n  - `thumbnail`: (string) user avatar URI\n- `videoIds`: (Array\u003c`string`\u003e) the Ids of the videos representing the new content of the queue.\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003eon('playlistAdded', (event) =\u003e { ... });\u003c/code\u003e\u003c/summary\u003e\n\u003cbr /\u003e\n\u003cp\u003e\nEmitted when a list of videos has been added to the queue.\n\u003c/p\u003e\n\n`event`: (object)\n- `type`: (string) 'playlistAdded'\n- `user`: (object) where applicable, the user that triggered the event.\n  - `name`: (string) name of the user\n  - `thumbnail`: (string) user avatar URI\n- `videoIds`: (Array\u003c`string`\u003e) the Ids of the videos added to the queue.\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003eon('playlistCleared', (event) =\u003e { ... });\u003c/code\u003e\u003c/summary\u003e\n\u003cbr /\u003e\n\u003cp\u003e\nEmitted when the queue has been cleared.\n\u003c/p\u003e\n\n`event`: (object)\n- `type`: (string) 'playlistCleared'\n- `user`: (object) where applicable, the user that triggered the event.\n  - `name`: (string) name of the user\n  - `thumbnail`: (string) user avatar URI\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003eon('playlistUpdated', (event) =\u003e { ... });\u003c/code\u003e\u003c/summary\u003e\n\u003cbr /\u003e\n\u003cp\u003e\nEmitted when the queue has been updated and the update is not associated with any of the aforementioned events.\n\u003c/p\u003e\n\n`event`: (object)\n- `type`: (string) 'playlistUpdated'\n- `videoIds`: (Array\u003c`string`\u003e) the Ids of the videos in the updated queue.\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003eon('autoplayModeChange', (previous, current) =\u003e { ... });\u003c/code\u003e\u003c/summary\u003e\n\u003cbr /\u003e\n\u003cp\u003e\nEmitted when the autoplay mode has changed.\n\u003c/p\u003e\n\n- `previous`: (string) Previous autoplay mode. One of [Constants.AUTOPLAY_MODES](#constants).\n- `current`: (string) Current autoplay mode. One of [Constants.AUTOPLAY_MODES](#constants).\n\n\u003c/details\u003e\n\n## Autoplay on Connect\n\nBy default, autoplay is enabled automatically on the sender app when a Cast session begins. You can disable this behaviour:\n\n```\n// At construction time\nconst receiver = new YouTubeCastReceiver(player, {\n  app: {\n    enableAutoplayOnConnect: false\n    ...\n  }\n});\n\n// At runtime\nreceiver.enableAutoplayOnConnect(false);\n```\n\n\u003e `enableAutoplayOnConnect: true` may be overridden depending on the autoplay capability of all connected senders. If any one sender doesn't support autoplay (e.g. YouTube website), then autoplay will be disabled for all senders.\n\n## Logging\n\nYou can set log level as follows:\n```\n// At construction time\nconst receiver = new YouTubeCastReceiver(player, {\n  logLevel: ...\n  ...\n});\n\n// At runtime\nreceiver.setLogLevel(...);\n```\n\nLog level can be one of [Constants.LOG_LEVELS](#constants).\n\n### Logger\n\nBy default, a `DefaultLogger` instance is used for logging, which prints logs to the console.\n\nYou can get the logger used by a receiver instance as follows:\n```\nconst logger = receiver.logger;\n```\n\n\u003cdetails\u003e\n\u003csummary\u003eImplementing your own logger\u003c/summary\u003e\n\u003cbr /\u003e\n\u003cp\u003e\nThe \u003ccode\u003eLogger\u003c/code\u003e interface defines the following methods you have to implement in your own logger:\n\n- \u003ccode\u003eerror(...msg: any[]): void;\u003c/code\u003e\u003cbr /\u003e\n- \u003ccode\u003ewarn(...msg: any[]): void;\u003c/code\u003e\u003cbr /\u003e\n- \u003ccode\u003einfo(...msg: any[]): void;\u003c/code\u003e\u003cbr /\u003e\n- \u003ccode\u003edebug(...msg: any[]): void;\u003c/code\u003e\u003cbr /\u003e\n- \u003ccode\u003esetLevel(value: LogLevel): void;\u003c/code\u003e\u003cbr /\u003e\n\u003c/p\u003e\n\nExample:\n\n```\n// ESM + Typescript:\nimport { Logger, LogLevel } from 'yt-cast-receiver';\n\nclass MyLogger implements Logger {\n  error(...msg: any[]): void {\n    ...\n  }\n  warn(...msg: any[]): void {\n    ...\n  }\n  info(...msg: any[]): void {\n    ...\n  }\n  debug(...msg: any[]): void {\n    ...\n  }\n  setLevel(value: LogLevel): void {\n    ...\n  }\n}\n\n// CJS; no Typescript\nclass MyLogger {\n  error(...msg) { ... }\n  warn(...msg) { ... }\n  info(...msg) { ... }\n  debug(...msg) { ... }\n  setLevel(value) { ... }\n}\n```\n\nTo use your logger implementation:\n```\nconst receiver = new YouTubeCastReceiver(player, {\n  logger: new MyLogger()\n  ...\n});\n```\n\nIf you just want to change the output destination of logs, you may consider extending the `DefaultLogger` class instead:\n\n```\nimport { DefaultLogger } from 'yt-cast-receiver';\n\nclass MyLogger extends DefaultLogger {\n\n  // Override DefaultLogger's toOutput() method\n  toOutput(targetLevel: LogLevel, msg: string[]): void {\n    // Send `msg` to destination of choice.\n    ...\n  }\n}\n```\n\u003c/details\u003e\n\n## DataStore\n\nA `DataStore` implementation is used to persist certain data during the startup phase of the receiver. The following lists the keys used to reference the persisted data and their purpose:\n\n|     Key     |     Description                 |\n|-------------|---------------------------------|\n|`app.pid`    |(string) Used by the DIAL server to broadcast the receiver's identity. If `pid` changes every time the receiver starts, you might see duplicate entries in the Cast menu.|\n|`mdxContext` |(object: {`screenId`, `deviceId`}) Identifies the 'screen' being cast to. You can think of a screen as something to which senders connect, and thus controlled by users, and on which the receiver plays content. Preserving the `mdxContext`facilitates reconnections in case a Cast session got interrupted, such as when the receiver stops abnormally and needs to be restarted.\n\nTo disable persistence, set the `dataStore` option to `false` when creating the receiver instance:\n\n```\nconst receiver = new YouTubeCastReceiver({\n  ...\n  dataStore: false\n});\n```\n\nBy default, a `DefaultDataStore` instance is used to persist data. It saves data to local files using the [node-persist](https://github.com/simonlast/node-persist) module. You can provide your own implementation by extending the `DataStore` class:\n\n```\nimport { DataStore } from 'yt-cast-receiver';\n\nclass MyDataStore extends DataStore {\n\n  // Store `value` referenced by `key`.\n  async set\u003cT\u003e(key: string, value: T): Promise\u003cvoid\u003e {\n    ...\n  }\n\n  // Return stored value by `key`, or `null` if none found.\n  async get\u003cT\u003e(key: string): Promise\u003cT | null\u003e {\n    ...\n  }\n}\n\n```\nAlthough not required, it is recommended that you provide a method in your implementation to clear the stored data. This would allow the receiver to start afresh.\n\nThe `DefaultDataStore`, for example, provides a `clear()` method:\n```\nimport { DefaultDataStore } from \"yt-cast-receiver\";\n\nconst defaultDataStore = new DefaultDataStore();\nawait defaultDataStore.clear();\n\nconst receiver = new YouTubeCastReceiver({\n  ...\n  dataStore: defaultDataStore\n});\n\nreceiver.start(); // Fresh start!\n```\n\n## Mute Policy\n\nWhen volume is muted, its original level remains unchanged. Player implementations shall disable audio output while playing content.\n\nOn unmute, the original volume level is restored. Player implementations shall resume audio output.\n\nHowever, not all sender apps support mute. On apps that don't, the volume level is set to 0 to mimic mute. Known sender apps that support mute are:\n\n1. YouTube desktop website\n2. YouTube Music desktop website\n\nKnown sender apps that do not support mute are:\n\n1. YouTube mobile app\n2. YouTube Music mobile app\n\nThe entry point to setting the volume level and muted status on the receiver is `setVolume(volume)` of the `Player` API, where `volume` is an object with the following properties:\n\n- `level`: (number) the volume level to set (0-100).\n- `muted`: (boolean) muted status.\n\nFor example, when the receiver receives a message from a sender telling it to set the volume, say `level: 50` and `muted: true`, it will call `setVolume()` as follows:\n\n```\nplayer.setVolume({ level: 50, muted: true });\n```\n\nThis will in turn call:\n```\nplayer.doSetVolume({ level: 50, muted: true });\n```\n\nYour player implementation will then do whatever is necessary to set the volume level and muted status on the receiver device. After that, the receiver will send a volume change notification back to all connected sender apps. The sender apps will then update their UI to show the new volume.\n\nHowever, as previously mentioned, not all sender apps support mute. In the current scenario, if one of the sender apps does not support mute, then it will ignore `muted: true` in the volume change notification payload and simply show '50' as the current volume level.\n\nTo resolve this, the receiver checks for the presence of sender apps that do not support mute. If it finds any, instead of doing the following in `setVolume()`:\n```\nsetVolume() {\n  ...\n  this.doSetVolume({ level: 50, muted: true });\n}\n```\n\nIt will do this:\n```\nsetVolume() {\n  ...\n  this.doSetVolume({ level: 0, muted: true });\n}\n```\n\nThen, when the volume change notification is sent back to sender apps, it will indicate current volume level as 0. Sender apps that do not support mute will still show the correct volume in their UI.\n\nObviously, the drawback in this approach is that when you click 'unmute' in sender apps that do support mute, the original volume level cannot be restored because it has already been set to 0. If you do not want this effect, and prefer to have sender apps without mute support to show a non-zero volume level when muted, you can specify `mutePolicy` when constructing the receiver instance:\n\n```\nconst receiver = this.#receiver = new YouTubeCastReceiver(player, {\n  ...\n  app: {\n    mutePolicy: Constants.MUTE_POLICIES.PRESERVE_VOLUME_LEVEL\n  }\n});\n```\n\n\u003e The default mute policy is `MUTE_POLICIES.AUTO`, which is what has been described above.\n\nOn the other hand, if you want to set volume level to 0, regardless of whether sender apps support mute, you can set `mutePolicy` to `MUTE_POLICIES.ZERO_VOLUME_LEVEL`.\n\n## Reset Player On Disconnect Policy\n\nBy default, the receiver resets the player when all senders disconnect. This means any current playback is stopped and the player queue is cleared.\n\nYou can override this behavior such that the player is reset only when all senders are *explicitly* disconnected. A sender is explicitly disconnected when:\n\n- The user ends the Cast session by the tapping the 'Stop Casting' button in the sender app; or\n- The receiver has to end the Cast session due to incoming connection from a different client type (e.g. switching from YouTube to YouTube Music or vice versa) or occurrence of an irrecoverable error.\n\nAn example of where a sender is *implicitly* disconnected is where the sender device has gone outside the reach of the receiver's network. In such cases, the sender app should automatically reconnect when it rejoins the network.\n\nTo set the Reset Player On Disconnect Policy:\n\n```\n// Default policy is `Constants.RESET_PLAYER_ON_DISCONNECT_POLICIES.ALL_DISCONNECTED`\nconst policy = Constants.RESET_PLAYER_ON_DISCONNECT_POLICIES.ALL_EXPLICITLY_DISCONNECTED;\n\n// At construction time\nconst receiver = new YouTubeCastReceiver(player, {\n  app: {\n    resetPlayerOnDisconnectPolicy: policy\n    ...\n  }\n});\n\n// At runtime\nreceiver.setResetPlayerOnDisconnectPolicy(policy);\n```\n\n## Constants\n\nConstants are defined for convenience. For example:\n\n```\n// ESM\nimport { Constants } from 'yt-cast-receiver';\n// CJS\nconst { Constants } = require('yt-cast-receiver')\n\n// Instead of:\nreceiver.setLogLevel('error');\n\n// You may do this:\nreceiver.setLogLevel(Constants.LOG_LEVELS.ERROR);\n```\n\nThe following groups of constants are defined:\n\n\u003cdetails\u003e\n\u003csummary\u003eConstants.AUTOPLAY_MODES\u003c/summary\u003e\n\u003cbr /\u003e\n\u003cp\u003e\nUseful when checking current autoplay mode:\n\n```\nif (player.autoplayMode === Constants.AUTOPLAY_MODES.DISABLED) {\n  ...\n}\n```\n\u003c/p\u003e\n\n**Properties**\n- ENABLED\n- DISABLED\n- UNSUPPORTED\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eConstants.PLAYER_STATUSES\u003c/summary\u003e\n\u003cbr /\u003e\n\u003cp\u003e\nUseful when checking current player status, updating player state through `player.notifyExternalStateChange()`:\n\n```\nif (player.status === Constants.PLAYER_STATUSES.PLAYING) {\n  ...\n}\n```\n\u003c/p\u003e\n\n**Properties**\n- IDLE\n- PLAYING\n- PAUSED\n- LOADING\n- STOPPED\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eConstants.LOG_LEVELS\u003c/summary\u003e\n\u003cbr /\u003e\n\u003cp\u003e\nUseful when setting log level.\n\u003c/p\u003e\n\n**Properties**\n- ERROR\n- WARN\n- INFO\n- DEBUG\n- NONE\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eConstants.STATUSES\u003c/summary\u003e\n\u003cbr /\u003e\n\u003cp\u003e\nUseful when checking receiver statatus.\n```\nif (receiver.status === Constants.STATUSES.RUNNING) {\n  ...\n}\n```\n\u003c/p\u003e\n\n**Properties**\n- STOPPED\n- STOPPING\n- STARTING\n- RUNNING\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eConstants.CLIENTS\u003c/summary\u003e\n\u003cbr /\u003e\n\u003cp\u003e\nPredefined info corresponding to 'YouTube' or 'YouTube Music' client.\n\nTo check whether a sender is coming from YouTube Music:\n```\nif (sender.client === Constants.CLIENTS.YTMUSIC) {\n  ...\n}\n```\n\nTo check whether a video is cast from YouTube:\n```\n// In your player implementation\ndoPlay(video: Video...) {\n  if (video.client === Constants.CLIENTS.YT) {\n    ...\n  }\n}\n```\n\u003c/p\u003e\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eConstants.MUTE_POLICIES\u003c/summary\u003e\n\u003cbr /\u003e\n\u003cp\u003e\nMute policy stipulates whether volume level should be set to 0 when \u003ccode\u003eplayer.setVolume(volume)`\u003c/code\u003e is called with \u003ccode\u003e`volume.muted: true`\u003c/code\u003e.\n\u003c/p\u003e\n\n**Properties**\n- `ZERO_VOLUME_LEVEL`: set volume level to 0.\n- `PRESERVE_VOLUME_LEVEL`: do not override volume level.\n- `AUTO`: preserve volume level or set it to 0, depending on whether connected senders support mute.\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eConstants.RESET_PLAYER_ON_DISCONNECT_POLICIES\u003c/summary\u003e\n\u003cbr /\u003e\n\u003cp\u003e\nWhether to reset the player (stop current playback and clear player queue) when all senders have disconnected.\n\u003c/p\u003e\n\n**Properties**\n- `ALL_EXPLICITLY_DISCONNECTED`: Reset player only when all senders are *explicitly* disconnected.\n- `ALL_DISCONNECTED`: Reset player when all senders have disconnected, whether implicitly or explicitly.\n\u003c/details\u003e\n\n# API\n\nSee generated [API docs](docs/api).\n\n# Running the example\n\n```\n$ git clone https://github.com/patrickkfkan/yt-cast-receiver.git\n$ cd yt-cast-receiver\n$ npm install\n$ npm run example\n// or\n$ npm run example:no-ui\n```\nNote: demo uses port 8099.\n\n# Changelog\n\n1.3.3\n- Update YouTube.js lib\n\n1.3.2\n- Update YouTube.js lib\n\n1.3.1\n- Update YouTube.js lib\n- Add `GaiaId` props to `Sender`\n\n1.3.0\n- Update dependencies and libraries\n- This version now requires Node v18\n\n1.2.4\n- [Fixed] YouTube.js lib errors in example\n\n1.2.3\n- [Added] Include `previous` and `next` videos in `PlaylistState`\n- [Added] `playlistUpdated` and `autoplayModeChange` queue events\n\n1.2.2\n- [Fixed] Export of `setResetPlayerOnDisconnectPolicy()` in `YouTubeCastReceiver`\n\n1.2.1\n- [Fixed] API doc\n- [Fixed] Default export in `Constants`\n\n1.2.0\n- [Added] `implicit` flag in `senderDisconnect` event\n- [Added] `resetPlayerOnDisconnectPolicy` receiver option\n\n1.1.0\n- [Changed] On queue update, dismiss autoplay video first when it is expected to change.\n- [Fixed] Recurring autoplay videos\n- [Added] `user` property in `Sender`\n\n1.0.1\n- [Added] Queue events\n- [Fixed] Erratic seeking issues and status updates when casting from desktop website\n\n1.0.0\n- Complete rewrite with major breaking changes in the API from v0.1.x!\n- Move to Typescript and package as ESM + CJS hybrid module.\n- New features:\n  - Support casting from YouTube Music (credit: [dgalli1](https://github.com/dgalli1))\n  - Allow multiple sender connections\n  - Support manual pairing, aka Link with TV Code (YouTube only)\n  - Provide player implementations with sufficient data to play private media (such as uploads in your YouTube Music library)\n  - Improved logging\n\n0.1.2\n\n- [Added] More options (credit: [mas94uk](https://github.com/mas94uk))\n- [Fixed] Git repo dependency paths\n\n  0.1.1-b\n\n- [Changed] More robust fetching of mix playlists for 'Up Next' videos\n\n  0.1.0b\n\n- [Fixed] Connection issue with YouTube mobile app version 16.22.35 and later\n\n  0.1.0a\n\n- Initial release\n\n# License\n\nMIT\n\n*Note on commercial use*:\n\nThis project uses a [forked version](https://github.com/patrickkfkan/peer-dial) of [peer-dial](https://github.com/fraunhoferfokus/peer-dial) for DIAL server implementation, which is provided \"free for non commercial use\". This means if you want to use `yt-cast-receiver` in a commercial product (not recommended anyway), you should contact the author of the *peer-dial* module for consent.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpatrickkfkan%2Fyt-cast-receiver","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpatrickkfkan%2Fyt-cast-receiver","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpatrickkfkan%2Fyt-cast-receiver/lists"}