diff --git a/docs/configuration/cameras/README.md b/docs/configuration/cameras/README.md index 1036071b..6aeef734 100644 --- a/docs/configuration/cameras/README.md +++ b/docs/configuration/cameras/README.md @@ -114,7 +114,8 @@ cameras: ## `dimensions` -The `dimensions` block configures the dimensions and layout of media of a given camera. +The `dimensions` block configures the dimensions and layout of media of a given camera (see [Card Dimensions](../dimensions.md) to set the dimensions of the whole card and not just a single camera). + ```yaml cameras: @@ -130,8 +131,7 @@ cameras: ### Layout Configuration -The `layout` block configures the fit and position of the media _within_ the camera dimensions (in order to control the dimensions for the whole card see [the card dimensions configuration](../dimensions.md) ). As the default behavior is to always expand to fit the media precisely, these options only make sense if the camera `dimensions.aspect_ratio` is set to a static value that forces a particular aspect ratio that does not match the camera media. - +The `layout` block configures the fit and position of the media _within_ the camera dimensions (in order to control the dimensions for the whole card see [the card dimensions configuration](../dimensions.md) ). ```yaml cameras: @@ -143,11 +143,32 @@ cameras: | Option | Default | Description | | - | - | - | -| `fit` | `contain` | If `contain`, the media is contained within the card and letterboxed if necessary. If `cover`, the media is expanded proportionally (i.e. maintaining the media aspect ratio) until the card is fully covered. If `fill`, the media is stretched to fill the card (i.e. ignoring the media aspect ratio). See [CSS object-fit](https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit) for technical details and a visualization. | -| `position` | | A dictionary that contains an `x` and `y` percentage (`0` - `100`) to control the position of the media when the fit is `cover`. This can be effectively used to "pan" the media around. At any given time, only one of `x` and `y` will have an effect, depending on whether media width is larger than the card width (in which case `x` controls the position) or the media height is larger than the card height (in which case `y` controls the position). A value of `0` means maximally to the left or top of the media, a value of `100` means maximally to the right or bottom of the media. See [CSS object-position](https://developer.mozilla.org/en-US/docs/Web/CSS/object-position) for technical details and a visualization. | +| `fit` | `contain` | If `contain`, the media is contained within the card and letterboxed if necessary. If `cover`, the media is expanded proportionally (i.e. maintaining the media aspect ratio) until the camera/card dimensions are fully covered. If `fill`, the media is stretched to fill the camera/card dimensions (i.e. ignoring the media aspect ratio). See [CSS object-fit](https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit) for technical details and a visualization. | +| `position` | | A dictionary that may contain an `x` and `y` percentage (`0` - `100`) to control the position of the media when the fit is `cover` (for other values of `fit` this option has no effect). This can be effectively used to "pan"/cut the media shown. At any given time, only one of `x` and `y` will have an effect, depending on whether media width is larger than the camera/card dimensions (in which case `x` controls the position) or the media height is larger than the camera/card dimensions (in which case `y` controls the position). A value of `0` means maximally to the left or top of the media, a value of `100` means maximally to the right or bottom of the media. See [CSS object-position](https://developer.mozilla.org/en-US/docs/Web/CSS/object-position) for technicals. See visualizations below. | +| `view_box` | | A dictionary that may contain a `top`, `bottom`, `left` and `right` percentage (`0` - `100`) to precisely crop what part of the media to show by specifying a % inset value from each side. Browsers apply this cropping after `position` and `fit` have been applied. See visualizations below. | + +!> The `view_box` parameter only has effect on browsers that support [`object-view-box`](https://caniuse.com/mdn-css_properties_object-view-box). As of writing this is exclusively Chromium based browsers (e.g. Google Chrome or Microsoft Edge). See [media layout examples](../../examples.md?id=media-layout). +#### Layout **Visualizations** + +##### `fit` + +![](../../images/media_layout/fit.png "Media Layout Fit :size=400") + +##### `position`: When media is shorter than dimensions height + +![](../../images/media_layout/position-shorter-than-height.png "Media Layout Position: Wider than taller :size=400") + +##### `position`: When media is thinner than dimensions width + +![](../../images/media_layout/position-thinner-than-width.png "Media Layout Position: Taller than wider :size=400") + +#### `view_box`: Precise media cropping + +![](../../images/media_layout/view-box.png "Media Layout Position: Taller than wider :size=400") + ## `triggers` The `triggers` block configures what triggers a camera. Triggering can be used diff --git a/docs/configuration/dimensions.md b/docs/configuration/dimensions.md index 2e60fdd8..45338087 100644 --- a/docs/configuration/dimensions.md +++ b/docs/configuration/dimensions.md @@ -1,10 +1,12 @@ # `dimensions` -These options control the dimensions and aspect-ratio of the card. These options -configuration applies once to the entire card (including the menu, thumbnails, -etc), not just to displayed media. This only applies to the card in normal -render mode -- when in fullscreen, or when in expanded (popup/dialog mode) the -aspect ratio is chosen dynamically to maximize the amount of content shown. +These options control the dimensions and aspect-ratio of the card (See [Camera +Dimensions](./cameras/README.md?id=dimensions) to set the dimensions of a camera +and not the whole card). These options configuration applies once to the entire +card (including the menu, thumbnails, etc), not just to displayed media. This +only applies to the card in normal render mode -- when in fullscreen, or when in +expanded (popup/dialog mode) the aspect ratio is chosen dynamically to maximize +the amount of content shown. ```yaml dimensions: diff --git a/docs/images/media_layout/fit.png b/docs/images/media_layout/fit.png new file mode 100644 index 00000000..b47fe96a Binary files /dev/null and b/docs/images/media_layout/fit.png differ diff --git a/docs/images/media_layout/position-shorter-than-height.png b/docs/images/media_layout/position-shorter-than-height.png new file mode 100644 index 00000000..e4804d12 Binary files /dev/null and b/docs/images/media_layout/position-shorter-than-height.png differ diff --git a/docs/images/media_layout/position-thinner-than-width.png b/docs/images/media_layout/position-thinner-than-width.png new file mode 100644 index 00000000..2f171db2 Binary files /dev/null and b/docs/images/media_layout/position-thinner-than-width.png differ diff --git a/docs/images/media_layout/view-box.png b/docs/images/media_layout/view-box.png new file mode 100644 index 00000000..9a91d96a Binary files /dev/null and b/docs/images/media_layout/view-box.png differ diff --git a/package.json b/package.json index f72013e0..561a83f0 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "lodash-es": "^4.17.21", "masonry-layout": "^4.2.2", "moment": "^2.29.4", + "p-queue": "^8.0.1", "propagating-hammerjs": "^2.0.1", "quick-lru": "^6.1.0", "screenfull": "^6.0.2", @@ -89,6 +90,7 @@ "build": "yarn run lint && yarn run rollup", "docs": "docsify serve ./docs", "docs-check-links": "find ./docs -name '*.md' -print0 | xargs -0 -n1 markdown-link-check -c ./markdown-link-check.json", + "docs-update-images": "./scripts/update-images.sh", "lint": "eslint 'src/**/*.ts'", "rollup": "rollup -c", "prune": "ts-prune", diff --git a/scripts/update-images.sh b/scripts/update-images.sh new file mode 100755 index 00000000..753c78be --- /dev/null +++ b/scripts/update-images.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +WIDTH=800 + +URL_FIT="https://docs.google.com/drawings/d/e/2PACX-1vTq0SVS8HWs3jGC0jjNpJoYfMbZS6P27CYyPlDhSa9OhdB_3jEb0HTLLYwu8Nv3J1TdjAJppcjTiVNy/pub?w=$WIDTH" +URL_THINNER_THAN_WIDTH="https://docs.google.com/drawings/d/e/2PACX-1vRkMd89N0tkZt5IghPKhR6gs8zMhB-5_hx5QfP6BCxbsSIga_h44IczP06Sj_YnKkxhe0lRdeR-uh04/pub?w=$WIDTH" +URL_SHORTER_THAN_HEIGHT="https://docs.google.com/drawings/d/e/2PACX-1vTKVsXEWIbj9lYKrCeugdLKcK_rOwAZZDK8IzhPdHH4wMwV2v7kEI0nsn2Qgugb00qDVHsE7kE8CBIC/pub?w=$WIDTH" +URL_VIEW_BOX="https://docs.google.com/drawings/d/e/2PACX-1vRpPNsaStxW2ENmv1kfUQg41cua9XQ2sQziq2PC8LdRCtkvjHSKYH3CyPO1Pz7kOdiQ2yQKrBX88-TF/pub?w=$WIDTH" + +DIR_MEDIA_LAYOUT_IMAGES="./docs/images/media_layout" + +wget -q -O "$DIR_MEDIA_LAYOUT_IMAGES/fit.png" "$URL_FIT" && \ + wget -q -O "$DIR_MEDIA_LAYOUT_IMAGES/position-thinner-than-width.png" "$URL_THINNER_THAN_WIDTH" && \ + wget -q -O "$DIR_MEDIA_LAYOUT_IMAGES/position-shorter-than-height.png" "$URL_SHORTER_THAN_HEIGHT" && \ + wget -q -O "$DIR_MEDIA_LAYOUT_IMAGES/view-box.png" "$URL_VIEW_BOX" + diff --git a/src/camera-manager/manager.ts b/src/camera-manager/manager.ts index df7775fa..fa683cf8 100644 --- a/src/camera-manager/manager.ts +++ b/src/camera-manager/manager.ts @@ -1,6 +1,7 @@ import add from 'date-fns/add'; import cloneDeep from 'lodash-es/cloneDeep'; import sum from 'lodash-es/sum'; +import PQueue from 'p-queue'; import { CardCameraAPI } from '../card-controller/types.js'; import { CameraConfig, CamerasConfig, PTZAction, PTZPhase } from '../config/types.js'; import { MEDIA_CHUNK_SIZE_DEFAULT } from '../const.js'; @@ -108,6 +109,7 @@ export class CameraManager { protected _api: CardCameraAPI; protected _engineFactory: CameraManagerEngineFactory; protected _store: CameraManagerStore; + protected _initializationLimit = new PQueue({ concurrency: 1 }); constructor( api: CardCameraAPI, @@ -138,8 +140,6 @@ export class CameraManager { return false; } - await this.reset(); - // For each camera merge the config (which has no defaults) into the camera // global config (which does have defaults). The merging must happen in this // order, to ensure that the defaults in the cameras global config do not @@ -148,8 +148,16 @@ export class CameraManager { recursivelyMergeObjectsNotArrays({}, cloneDeep(config?.cameras_global), camera), ); - try { + const resetAndInitialize = async () => { + await this.reset(); await this._initializeCameras(cameras); + }; + + try { + // This concurrency limit prevents multiple rapidly arriving configs from + // generating reset-n-initialize race conditions (e.g. changing values + // rapidly in the config editor). + await this._initializationLimit.add(resetAndInitialize); } catch (e: unknown) { this._api.getMessageManager().setErrorIfHigherPriority(e); return false; diff --git a/src/config/types.ts b/src/config/types.ts index 7692d2ec..9c08c061 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -618,6 +618,14 @@ const mediaLayoutConfigSchema = z.object({ y: z.number().min(0).max(100).optional(), }) .optional(), + view_box: z + .object({ + bottom: z.number().min(0).max(100).optional().default(0), + left: z.number().min(0).max(100).optional().default(0), + right: z.number().min(0).max(100).optional().default(0), + top: z.number().min(0).max(100).optional().default(0), + }) + .optional(), }); export type MediaLayoutConfig = z.infer; diff --git a/src/const.ts b/src/const.ts index 6a497438..d39d45aa 100644 --- a/src/const.ts +++ b/src/const.ts @@ -61,6 +61,14 @@ export const CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_POSITION_X = `${CONF_CAMERAS}.#.dimensions.layout.position.x` as const; export const CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_POSITION_Y = `${CONF_CAMERAS}.#.dimensions.layout.position.y` as const; +export const CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_VIEW_BOX_TOP = + `${CONF_CAMERAS}.#.dimensions.layout.view_box.top` as const; +export const CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_VIEW_BOX_BOTTOM = + `${CONF_CAMERAS}.#.dimensions.layout.view_box.bottom` as const; +export const CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_VIEW_BOX_LEFT = + `${CONF_CAMERAS}.#.dimensions.layout.view_box.left` as const; +export const CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_VIEW_BOX_RIGHT = + `${CONF_CAMERAS}.#.dimensions.layout.view_box.right` as const; export const CONF_CAMERAS_ARRAY_TRIGGERS_MOTION = `${CONF_CAMERAS}.#.triggers.motion` as const; export const CONF_CAMERAS_ARRAY_TRIGGERS_OCCUPANCY = diff --git a/src/editor.ts b/src/editor.ts index 9502d69f..0b9adc74 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -46,6 +46,10 @@ import { CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_FIT, CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_POSITION_X, CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_POSITION_Y, + CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_VIEW_BOX_BOTTOM, + CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_VIEW_BOX_LEFT, + CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_VIEW_BOX_RIGHT, + CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_VIEW_BOX_TOP, CONF_CAMERAS_ARRAY_FRIGATE_CAMERA_NAME, CONF_CAMERAS_ARRAY_FRIGATE_CLIENT_ID, CONF_CAMERAS_ARRAY_FRIGATE_LABELS, @@ -1226,6 +1230,10 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor configPathFit: string, configPathPositionX: string, configPathPositionY: string, + configPathPositionTop: string, + configPathPositionBottom: string, + configPathPositionLeft: string, + configPathPositionRight: string, ): TemplateResult | void { return this._putInSubmenu( domain, @@ -1236,16 +1244,50 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor ${this._renderOptionSelector(configPathFit, this._layoutFits, { label: localize('config.cameras.dimensions.layout.fit'), })} - ${this._renderNumberInput(configPathPositionX, { - min: 0, - max: 100, - label: localize('config.cameras.dimensions.layout.position.x'), - })} - ${this._renderNumberInput(configPathPositionY, { - min: 0, - max: 100, - label: localize('config.cameras.dimensions.layout.position.y'), - })} + ${this._putInSubmenu( + `${domain}.position`, + true, + 'config.cameras.dimensions.layout.position.editor_label', + { name: 'mdi:aspect-ratio' }, + html` ${this._renderNumberInput(configPathPositionX, { + min: 0, + max: 100, + label: localize('config.cameras.dimensions.layout.position.x'), + })} + ${this._renderNumberInput(configPathPositionY, { + min: 0, + max: 100, + label: localize('config.cameras.dimensions.layout.position.y'), + })}`, + )} + ${this._putInSubmenu( + `${domain}.view_box`, + true, + 'config.cameras.dimensions.layout.view_box.editor_label', + { name: 'mdi:crop' }, + html` + ${this._renderNumberInput(configPathPositionTop, { + min: 0, + max: 100, + label: localize('config.cameras.dimensions.layout.view_box.top'), + })} + ${this._renderNumberInput(configPathPositionBottom, { + min: 0, + max: 100, + label: localize('config.cameras.dimensions.layout.view_box.bottom'), + })} + ${this._renderNumberInput(configPathPositionLeft, { + min: 0, + max: 100, + label: localize('config.cameras.dimensions.layout.view_box.left'), + })} + ${this._renderNumberInput(configPathPositionRight, { + min: 0, + max: 100, + label: localize('config.cameras.dimensions.layout.view_box.right'), + })} + `, + )} `, ); } @@ -1971,9 +2013,34 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor ${this._renderMediaLayout( MENU_CAMERAS_DIMENSIONS_LAYOUT, 'config.cameras.dimensions.layout.editor_label', - CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_FIT, - CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_POSITION_X, - CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_POSITION_Y, + getArrayConfigPath( + CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_FIT, + cameraIndex, + ), + getArrayConfigPath( + CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_POSITION_X, + cameraIndex, + ), + getArrayConfigPath( + CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_POSITION_Y, + cameraIndex, + ), + getArrayConfigPath( + CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_VIEW_BOX_TOP, + cameraIndex, + ), + getArrayConfigPath( + CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_VIEW_BOX_BOTTOM, + cameraIndex, + ), + getArrayConfigPath( + CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_VIEW_BOX_LEFT, + cameraIndex, + ), + getArrayConfigPath( + CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_VIEW_BOX_RIGHT, + cameraIndex, + ), )} `, )} diff --git a/src/localize/languages/ca.json b/src/localize/languages/ca.json index a822bf33..7890348b 100644 --- a/src/localize/languages/ca.json +++ b/src/localize/languages/ca.json @@ -56,8 +56,16 @@ "fill": "El suport s'estira per omplir la targeta" }, "position": { + "editor_label": "", "x": "Percentatge d'emplaçament horitzontal", "y": "Percentatge d'emplaçament vertical" + }, + "view_box": { + "bottom": "", + "editor_label": "", + "left": "", + "right": "", + "top": "" } } }, diff --git a/src/localize/languages/en.json b/src/localize/languages/en.json index f0c5df92..61b48940 100644 --- a/src/localize/languages/en.json +++ b/src/localize/languages/en.json @@ -56,8 +56,16 @@ "fill": "Media is stretched to fill the card" }, "position": { + "editor_label": "Layout Cover Position", "x": "Horizontal placement percentage", "y": "Vertical placement percentage" + }, + "view_box": { + "bottom": "Bottom inset percentage", + "editor_label": "Layout View Box", + "left": "Left inset percentage", + "right": "Right inset percentage", + "top": "Top inset percentage" } } }, @@ -616,4 +624,4 @@ "timeline": { "select_date": "Choose date" } -} +} \ No newline at end of file diff --git a/src/localize/languages/fr.json b/src/localize/languages/fr.json index fdc1107b..806ed481 100644 --- a/src/localize/languages/fr.json +++ b/src/localize/languages/fr.json @@ -56,8 +56,16 @@ "fill": "Il supporto viene allungato per riempire la scheda" }, "position": { + "editor_label": "", "x": "Percentuale di posizionamento orizzontale", "y": "Percentuale di posizionamento verticale" + }, + "view_box": { + "bottom": "", + "editor_label": "", + "left": "", + "right": "", + "top": "" } } }, diff --git a/src/localize/languages/it.json b/src/localize/languages/it.json index e515dc57..b55a68e3 100644 --- a/src/localize/languages/it.json +++ b/src/localize/languages/it.json @@ -56,8 +56,16 @@ "fill": "Il supporto viene allungato per riempire la scheda" }, "position": { + "editor_label": "", "x": "Percentuale di posizionamento orizzontale", "y": "Percentuale di posizionamento verticale" + }, + "view_box": { + "bottom": "", + "editor_label": "", + "left": "", + "right": "", + "top": "" } } }, diff --git a/src/localize/languages/pt-BR.json b/src/localize/languages/pt-BR.json index ad8fe495..fb1fb510 100644 --- a/src/localize/languages/pt-BR.json +++ b/src/localize/languages/pt-BR.json @@ -56,8 +56,16 @@ "fill": "A mídia é esticada para preencher o cartão" }, "position": { + "editor_label": "", "x": "Porcentagem do posicionamento horizontal", "y": "Porcentagem do posicionamento vertical" + }, + "view_box": { + "bottom": "", + "editor_label": "", + "left": "", + "right": "", + "top": "" } } }, diff --git a/src/localize/languages/pt-PT.json b/src/localize/languages/pt-PT.json index fb8f531c..2bee1366 100644 --- a/src/localize/languages/pt-PT.json +++ b/src/localize/languages/pt-PT.json @@ -56,8 +56,16 @@ "fill": "Preencher" }, "position": { + "editor_label": "", "x": "Percentagem da localização horizontal", "y": "Percentagem da localização vertical" + }, + "view_box": { + "bottom": "", + "editor_label": "", + "left": "", + "right": "", + "top": "" } } }, diff --git a/src/scss/media-layout.scss b/src/scss/media-layout.scss index cde335ec..ec93509a 100644 --- a/src/scss/media-layout.scss +++ b/src/scss/media-layout.scss @@ -2,4 +2,13 @@ object-fit: var(--frigate-card-media-layout-fit, contain); object-position: var(--frigate-card-media-layout-position-x, 50%) var(--frigate-card-media-layout-position-y, 50%); + + // This is supported on limited browsers: + // See: https://caniuse.com/?search=object-view-box + object-view-box: inset( + var(--frigate-card-media-layout-view-box-top, 0%) + var(--frigate-card-media-layout-view-box-right, 0%) + var(--frigate-card-media-layout-view-box-bottom, 0%) + var(--frigate-card-media-layout-view-box-left, 0%) + ); } diff --git a/src/utils/media-layout.ts b/src/utils/media-layout.ts index 017c1efe..356a8dd0 100644 --- a/src/utils/media-layout.ts +++ b/src/utils/media-layout.ts @@ -24,4 +24,14 @@ export const updateElementStyleFromMediaLayoutConfig = ( element.style.removeProperty(`--frigate-card-media-layout-position-${dimension}`); } } + for (const dimension of ['top', 'bottom', 'left', 'right']) { + if (mediaLayoutConfig?.view_box?.[dimension] !== undefined) { + element.style.setProperty( + `--frigate-card-media-layout-view-box-${dimension}`, + `${mediaLayoutConfig.view_box[dimension]}%`, + ); + } else { + element.style.removeProperty(`--frigate-card-media-layout-view-box-${dimension}`); + } + } }; diff --git a/yarn.lock b/yarn.lock index 4cbbfb1b..d7d556b0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3622,6 +3622,13 @@ __metadata: languageName: node linkType: hard +"eventemitter3@npm:^5.0.1": + version: 5.0.1 + resolution: "eventemitter3@npm:5.0.1" + checksum: 10c0/4ba5c00c506e6c786b4d6262cfbce90ddc14c10d4667e5c83ae993c9de88aa856033994dd2b35b83e8dc1170e224e66a319fa80adc4c32adcd2379bbc75da814 + languageName: node + linkType: hard + "execa@npm:^8.0.1": version: 8.0.1 resolution: "execa@npm:8.0.1" @@ -3886,6 +3893,7 @@ __metadata: markdown-link-check: "npm:^3.12.1" masonry-layout: "npm:^4.2.2" moment: "npm:^2.29.4" + p-queue: "npm:^8.0.1" prettier: "npm:^2.6.0" propagating-hammerjs: "npm:^2.0.1" quick-lru: "npm:^6.1.0" @@ -6234,6 +6242,16 @@ __metadata: languageName: node linkType: hard +"p-queue@npm:^8.0.1": + version: 8.0.1 + resolution: "p-queue@npm:8.0.1" + dependencies: + eventemitter3: "npm:^5.0.1" + p-timeout: "npm:^6.1.2" + checksum: 10c0/fe185bc8bbd32d17a5f6dba090077b1bb326b008b4ec9b0646c57a32a6984035aa8ece909a6d0de7f6c4640296dc288197f430e7394cdc76a26d862339494616 + languageName: node + linkType: hard + "p-timeout@npm:^3.1.0, p-timeout@npm:^3.2.0": version: 3.2.0 resolution: "p-timeout@npm:3.2.0" @@ -6243,6 +6261,13 @@ __metadata: languageName: node linkType: hard +"p-timeout@npm:^6.1.2": + version: 6.1.2 + resolution: "p-timeout@npm:6.1.2" + checksum: 10c0/d46b90a9a5fb7c650a5c56dd5cf7102ea9ab6ce998defa2b3d4672789aaec4e2f45b3b0b5a4a3e17a0fb94301ad5dd26da7d8728402e48db2022ad1847594d19 + languageName: node + linkType: hard + "p-try@npm:^2.0.0": version: 2.2.0 resolution: "p-try@npm:2.2.0"