Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
40174e3
feat(video-player): 🎥 integrate media-chrome for enhanced video playback
rogerantony-dev Feb 22, 2026
96ed2cf
feat(video-player): 🎨 add new icons and enhance video player controls
rogerantony-dev Feb 23, 2026
1963385
fix(video-player): 🛠️ enhance layout and visibility of video player c…
rogerantony-dev Feb 23, 2026
f744741
fix(video-player): 🛠️ prevent slider drags from triggering lightbox n…
rogerantony-dev Feb 23, 2026
9196e6f
chore(cspell): 📝 add new project-specific words to cspell dictionary
rogerantony-dev Feb 23, 2026
c2ff0c2
chore(dependencies): update media-chrome to version 4.18.0 in package…
rogerantony-dev Feb 23, 2026
d052e14
refactor(video-player): 🛠️ remove isActive prop from VideoPlayer
rogerantony-dev Feb 23, 2026
e26c1af
fix(video-player): 🛠️ improve YouTube video loading logic and adjust …
rogerantony-dev Feb 23, 2026
51bdeb3
fix(video-player): 🛠️ enable autoPlay for YouTube and HTML5 videos
rogerantony-dev Feb 23, 2026
fe646ea
style(video-player): 🎨 add invisible bridge for improved layout betwe…
rogerantony-dev Feb 23, 2026
476e8c6
refactor(video-player): 🛠️ simplify SettingsIcon component by removin…
rogerantony-dev Feb 23, 2026
1a3a9ac
fix(video-player): 🛠️ ensure media reference is cleared on error even…
rogerantony-dev Feb 23, 2026
109d6c7
fix(video-player): 🛠️ handle errors during YouTube video import to im…
rogerantony-dev Feb 23, 2026
5edcdf7
feat(video-player): 🎥 add isActive prop to control video playback state
rogerantony-dev Feb 23, 2026
63a8ac5
fix(video-player): 🛠️ update event handler to use onPointerDown for b…
rogerantony-dev Feb 23, 2026
6a31b7e
feat(video-player): 🎨 add MEDIA_STYLE for responsive video element si…
rogerantony-dev Feb 23, 2026
fae593c
style(video-player): 🎨 update PiP visibility comment for clarity on b…
rogerantony-dev Feb 23, 2026
5e5d77d
feat(video-player): 🎥 add loading spinner and bottom gradient
rogerantony-dev Feb 23, 2026
f57a308
style(video-player): 🎨 adjust bottom gradient height
rogerantony-dev Feb 23, 2026
fe985b8
feat(media-player): 🎥 implement MediaPlayer component for audio, vid…
rogerantony-dev Feb 24, 2026
8cbe01c
style(media-player): 🎨 update media control button backgrounds to tra…
rogerantony-dev Feb 27, 2026
c7ce44a
style(media-player): 🎨 add font-family to media time and preview disp…
rogerantony-dev Feb 27, 2026
4fdbcd2
Merge branch 'dev' into feat/media-chrome-video-player
rogerantony-dev Feb 27, 2026
5d12bb2
Merge branch 'dev' into feat/media-chrome-video-player
rogerantony-dev Feb 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@
"jwt-decode": "4.0.0",
"lodash": "4.17.23",
"match-sorter": "8.2.0",
"media-chrome": "4.17.2",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check latest versions on npm
echo "=== media-chrome latest version ==="
curl -s https://registry.npmjs.org/media-chrome/latest | jq '.version'

echo ""
echo "=== youtube-video-element latest version ==="
curl -s https://registry.npmjs.org/youtube-video-element/latest | jq '.version'

echo ""
echo "=== media-chrome package info ==="
curl -s https://registry.npmjs.org/media-chrome/4.17.2 | jq '{version: .version, published: .time["4.17.2"]}'

echo ""
echo "=== youtube-video-element package info ==="
curl -s https://registry.npmjs.org/youtube-video-element/1.9.0 | jq '{version: .version, published: .time["1.9.0"]}'

Repository: timelessco/recollect

Length of output: 334


🏁 Script executed:

#!/bin/bash
# Check for vulnerabilities using npm audit for the specific versions
echo "=== Checking media-chrome 4.17.2 ==="
npm view media-chrome@4.17.2 --json | jq '{version, homepage, license, _lastPublishedTime}'

echo ""
echo "=== Checking youtube-video-element 1.9.0 ==="
npm view youtube-video-element@1.9.0 --json | jq '{version, homepage, license, _lastPublishedTime}'

echo ""
echo "=== media-chrome changelog (4.17.2 to 4.18.0) ==="
npm view media-chrome versions --json | jq '.[-3:]'

Repository: timelessco/recollect

Length of output: 1220


🌐 Web query:

media-chrome 4.18.0 release notes changelog

💡 Result:

media-chrome 4.18.0 — changelog (released 2026-02-19)

Features

  • [a11y + i18n] Translate time phrases (PR #1232) [1]

Bug fixes

  • i18n aria-label fixes for time range and seek buttons (PR #1253) [1]
  • Firefox accessibility test fix (PR #1258) [1]
  • Patch memory leaks (PR #1261) [1]
  • Thumbnail preview sizing fix (PR #1262), closes #1160 [1]

Upstream compare link: v4.17.2...v4.18.0 [1]


Update media-chrome to 4.18.0. The current version 4.17.2 is outdated. The latest stable release includes memory leak patches, accessibility improvements, and bug fixes. youtube-video-element@1.9.0 is current.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` at line 121, Update the media-chrome dependency entry in
package.json from "media-chrome": "4.17.2" to "media-chrome": "4.18.0"; after
changing the version run your package manager to regenerate the lockfile (npm
install or yarn install) and run the test/build to verify there are no
regressions, and commit the updated package.json and lockfile changes.

"motion": "12.29.2",
"next": "16.1.6",
"next-themes": "0.4.6",
Expand All @@ -134,14 +135,14 @@
"react-infinite-scroll-component": "6.1.1",
"react-mentions": "4.4.10",
"react-modern-drawer": "1.4.0",
"react-player": "3.4.0",
"react-stately": "3.43.0",
"react-toastify": "11.0.5",
"resend": "6.9.1",
"slugify": "1.6.6",
"tailwind-merge": "3.4.0",
"uniqid": "5.4.0",
"yet-another-react-lightbox": "3.28.0",
"youtube-video-element": "1.9.0",
"zod": "4.3.6",
"zustand": "5.0.10"
},
Expand Down
442 changes: 14 additions & 428 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

164 changes: 145 additions & 19 deletions src/components/VideoPlayer.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,151 @@
import React, { useRef } from "react";
import ReactPlayer from "react-player";
"use client";

export const VideoPlayer = ({
src,
isActive,
onError,
}: {
import { useCallback, useEffect, useRef } from "react";
import {
MediaControlBar,
MediaController,
MediaFullscreenButton,
MediaMuteButton,
MediaPipButton,
MediaPlayButton,
MediaPreviewChapterDisplay,
MediaPreviewThumbnail,
MediaPreviewTimeDisplay,
MediaTimeDisplay,
MediaTimeRange,
MediaVolumeRange,
} from "media-chrome/react";
import { MediaProvider, useMediaRef } from "media-chrome/react/media-store";
import {
MediaPlaybackRateMenu,
MediaPlaybackRateMenuButton,
} from "media-chrome/react/menu";

import {
FullscreenIcon,
MuteIcon,
PipIcon,
PlayPauseIcon,
SettingsIcon,
} from "./video-player-icons";
import {
CONTROL_BAR_STYLE,
CONTROLLER_STYLE,
YOUTUBE_CONTROLLER_STYLE,
} from "./video-player-theme";

import "./video-player-theme.css";

export interface VideoPlayerProps {
isActive: boolean;
src: string;
isYouTube?: boolean;
onError?: () => void;
}) => {
const playerRef = useRef<HTMLVideoElement | null>(null);
src: string;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

isActive prop is accepted but never used — video won't pause when navigating away.

VideoPlayerProps declares isActive and callers pass it (e.g., LightboxRenderers.tsx passes isActive={isActive ?? false}), but it's never read in VideoPlayerInner or VideoPlayer. With the previous react-player implementation, isActive likely controlled playing state. Without it, video playback continues when the user navigates to a different lightbox slide.

You need to wire isActive to pause/play the media element — for example, via useEffect calling el.play()/el.pause() on the video ref, or using media-chrome's store API.

Also applies to: 138-144

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/VideoPlayer.tsx` around lines 35 - 40, VideoPlayerProps
includes isActive but the components never use it, so media keeps playing when a
slide becomes inactive; update VideoPlayer and/or VideoPlayerInner to watch
isActive (via useEffect) and control playback: get the video element ref used in
the component (the same ref passed to the media element), and when isActive
becomes true call el.play() (handle the Promise) and when false call el.pause();
for embedded YouTube (isYouTube) integrate with the player API or pause via
postMessage/iframe API, and call onError if play() rejects; ensure you guard
null refs and do not block UI while awaiting play().


/* ---- Main player ---- */

function VideoPlayerInner({
isYouTube,
onError,
src,
}: Omit<VideoPlayerProps, "isActive">) {
const mediaRef = useMediaRef();
const onErrorRef = useRef(onError);
onErrorRef.current = onError;

useEffect(() => {
if (isYouTube) {
void import("youtube-video-element");
}
}, [isYouTube]);

const ref = useCallback(
(el: HTMLVideoElement | null) => {
mediaRef(el);

if (!el) {
return undefined;
}

const handleError = () => onErrorRef.current?.();
el.addEventListener("error", handleError);
return () => el.removeEventListener("error", handleError);
},
[mediaRef],
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

React 19 ref callback cleanup function returns prevents null call unmount

💡 Result:

In React 19, a callback ref may return a cleanup function:

<div
  ref={(node) => {
    if (!node) return;          // (optional) ignore detach
    // setup using node
    return () => {
      // cleanup using the same node
    };
  }}
/>

What changes vs older React:

  • If your ref callback returns a cleanup function, React will call that cleanup when the ref is detached (including unmount), and it will skip calling your ref callback with null on unmount. This is the behavior you’re referring to (“return prevents the null call”). [1][2]
  • For backwards compatibility, if you don’t return a cleanup function, React still uses the old behavior and calls the ref callback with null when detaching—but React says this null-on-detach behavior will be removed in a future version. [2]
  • React also detaches/attaches when the ref callback identity changes (e.g., inline (node) => {} on every render). If you want to avoid unnecessary detach/attach cycles, keep the ref callback stable (e.g., useCallback). [2]

Sources: React 19 announcement + the ref callback docs on react.dev. [1][2]


🌐 Web query:

media-chrome useMediaRef documentation React cleanup null

💡 Result:

useMediaRef() is a React hook exported from media-chrome/react/media-store and is the intended way to “wire up” your <video>/<audio> (or compatible custom media element) to Media Chrome’s MediaStore (so useMediaSelector()/useMediaDispatch() work). You use it by passing it directly to the element’s ref prop. [1]

If you’re seeing cleanup errors like “cannot read … removeEventListener of null”, the common cause is that ref.current is mutable and may be set to null before your effect cleanup runs. The fix is to capture the element in a local variable inside the effect and use that for both add/remove. [2]

const mediaRef = useMediaRef();

useEffect(() => {
  const el = mediaRef.current; // capture once
  if (!el) return;

  const onTimeUpdate = () => {};
  el.addEventListener("timeupdate", onTimeUpdate);

  return () => {
    el.removeEventListener("timeupdate", onTimeUpdate);
  };
}, []);

Sources: Media Chrome “MediaStore React Hooks” docs (includes useMediaRef() usage) [1]; explanation of ref.current becoming null before cleanup + “capture in effect” pattern [2].


🏁 Script executed:

fd VideoPlayer.tsx

Repository: timelessco/recollect

Length of output: 94


🏁 Script executed:

# Once we have the file path, examine it
if [ -f "src/components/VideoPlayer.tsx" ]; then
  cat -n src/components/VideoPlayer.tsx | head -100
fi

Repository: timelessco/recollect

Length of output: 3003


🏁 Script executed:

# Search for media-chrome imports and useMediaRef usage
rg "useMediaRef|media-chrome" -B2 -A2 --type ts --type tsx

Repository: timelessco/recollect

Length of output: 91


🏁 Script executed:

rg "useMediaRef|from.*media-chrome" -t ts -t js

Repository: timelessco/recollect

Length of output: 360


🌐 Web query:

media-chrome MediaStore useMediaRef cleanup null unregister unmount

💡 Result:

In media-chrome/react/media-store, the intended “cleanup/unregister on unmount” behavior is usually driven by the ref callback receiving null (React calls a callback ref with null when the element unmounts or the ref moves).

So the key is: use the callback ref returned by useMediaRef() directly on your media element, and don’t “hide” it behind another ref pattern that prevents the null call from reaching MediaStore.

import { MediaProvider, useMediaRef } from "media-chrome/react/media-store";

function Player() {
  const mediaRef = useMediaRef(); // callback ref

  return (
    <MediaProvider>
      <video ref={mediaRef} />
    </MediaProvider>
  );
}

Common gotchas that can break cleanup:

  • Don’t convert it to an object ref (useRef) and only copy .current; you’ll miss the null callback.
  • Don’t wrap it in a callback that ignores null:
    const mediaRef = useMediaRef();
    const bad = (el) => { if (el) mediaRef(el); }; // blocks cleanup
  • If you need composition, forward null too:
    const mediaRef = useMediaRef();
    const composed = (el) => { mediaRef(el); /* your logic */ };

If you’re chasing leaks specifically: Media Chrome has been actively patching “disconnect/unmount” cleanup (removing listeners, disconnecting observers, etc.) in recent work (merged Feb 19, 2026). If you’re on an older media-chrome version, upgrading may matter. (github.com)

References:

  • media-chrome exports the React media-store entrypoint (./react/media-store). (app.unpkg.com)
  • Example usage of useMediaRef from media-chrome/react/media-store in the wild. (diceui.com)

Citations:


🌐 Web query:

"media-chrome" "useMediaRef" source code implementation state cleanup

💡 Result:

In media-chrome, the React hook useMediaRef() is used as a ref callback you compose onto your <video>/<audio> element so the Media Store can be told when the backing media element changes (example usage shows const mediaRefCallback = useMediaRef(); ... ref={useComposedRefs(..., mediaRefCallback)}), which in turn triggers the store’s “media element changed” flow. [1]

On the Media Store side, the key “state cleanup” logic for custom state wiring lives in createMediaStore()updateStateOwners() inside src/js/media-store/media-store.ts. Each state in the stateMediator can define stateOwnersUpdateHandlers (setup hooks that may return teardown functions). When any “state owner” changes (media element, textTracks, root node, etc.), the store:

  • decides whether it must teardown and/or setup monitoring (teardownSomething / setupSomething),
  • runs teardowns, then runs setups and stores the returned teardown(s). [2]

A bug existed where only the last teardown returned by stateOwnersUpdateHandlers was being kept/called, leaking earlier listeners. That was fixed and merged Nov 17, 2025 (“Ensure all stateOwnersUpdateHandlers are properly torn down”). The fix:

  • stores all teardown functions (if multiple handlers return them),
  • on subsequent updates, normalizes previous teardown(s) into an array and calls every function,
  • filters out non-functions,
  • stores the new teardown(s) as a single function or an array depending on count. [3][2]

Relevant code (current behavior):

  • teardown all previous teardowns (array-or-single) when teardownSomething is true, then
  • setup new handlers and persist teardowns (single-or-array). [2]

Sources: Dice UI docs showing useMediaRef() ref-callback usage pattern [1]; Media Store implementation showing stateOwnersUpdateHandlers teardown/setup logic [2]; PR #1237 describing/fixing the multi-teardown leak and merged date [3].

[1] (diceui.com)
[2] (raw.githubusercontent.com)
[3] (github.com)

Citations:


React 19 ref cleanup skips mediaRef(null)MediaStore unregister is blocked on unmount.

In React 19, returning a cleanup function from a ref callback causes React to call that cleanup on unmount instead of invoking the callback with null. The returned cleanup at line 73 only removes the error listener — it never calls mediaRef(null). This breaks the contract that useMediaRef() from media-chrome/react/media-store relies on: the callback ref must receive null to trigger proper MediaStore cleanup and unregister the element.

The intended pattern for useMediaRef() is to pass it directly as the ref or forward it in a composed ref. When wrapped in a callback that blocks the null call, the MediaStore retains a stale reference to the detached element.

🛠️ Proposed fix
 const ref = useCallback(
     (el: HTMLVideoElement | null) => {
         mediaRef(el);

         if (!el) {
             return undefined;
         }

         const handleError = () => onErrorRef.current?.();
         el.addEventListener("error", handleError);
-        return () => el.removeEventListener("error", handleError);
+        return () => {
+            el.removeEventListener("error", handleError);
+            mediaRef(null);
+        };
     },
     [mediaRef],
 );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const ref = useCallback(
(el: HTMLVideoElement | null) => {
mediaRef(el);
if (!el) {
return undefined;
}
const handleError = () => onErrorRef.current?.();
el.addEventListener("error", handleError);
return () => el.removeEventListener("error", handleError);
},
[mediaRef],
);
const ref = useCallback(
(el: HTMLVideoElement | null) => {
mediaRef(el);
if (!el) {
return undefined;
}
const handleError = () => onErrorRef.current?.();
el.addEventListener("error", handleError);
return () => {
el.removeEventListener("error", handleError);
mediaRef(null);
};
},
[mediaRef],
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/VideoPlayer.tsx` around lines 63 - 76, The callback ref
returned from useCallback (the ref assigned to the video element) currently
returns a cleanup that only removes the error listener, which in React 19
prevents mediaRef(null) from being called on unmount and blocks MediaStore
unregister; update the ref callback (the function stored in ref) so its cleanup
both removes the "error" listener (handleError) and explicitly calls
mediaRef(null) when cleaning up, and ensure the initial branch that handles el
being null still returns undefined; keep using onErrorRef.current for the
handler reference and preserve the existing listener add/remove logic.


return (
<MediaController
onPointerDownCapture={(event) => event.stopPropagation()}
style={isYouTube ? YOUTUBE_CONTROLLER_STYLE : CONTROLLER_STYLE}
>
{isYouTube ? (
<youtube-video crossOrigin="" ref={ref} slot="media" src={src} />
) : (
<video ref={ref} slot="media" src={src}>
<track default kind="captions" label="No captions" srcLang="en" />
</video>
)}

<MediaControlBar style={CONTROL_BAR_STYLE}>
<MediaPlayButton>
<PlayPauseIcon />
</MediaPlayButton>

<div className="mute-group">
<div className="mute-group-inner">
<MediaMuteButton ref={(el) => el?.setAttribute("notooltip", "")}>

Check warning on line 98 in src/components/VideoPlayer.tsx

View workflow job for this annotation

GitHub Actions / spellcheck

Unknown word (notooltip)

Check warning on line 98 in src/components/VideoPlayer.tsx

View workflow job for this annotation

GitHub Actions / spellcheck

Unknown word (notooltip)
<MuteIcon />
</MediaMuteButton>
<div className="vol-wrap">
<MediaVolumeRange />
</div>
</div>
</div>

<MediaTimeDisplay />
<MediaTimeDisplay showDuration />

<MediaTimeRange>
<MediaPreviewThumbnail slot="preview" />
<MediaPreviewChapterDisplay slot="preview" />
<MediaPreviewTimeDisplay slot="preview" />
</MediaTimeRange>

<div className="settings-group">
<div className="settings-group-inner">
<MediaPlaybackRateMenuButton>
<span
className="flex size-full items-center justify-center"
slot="icon"
>
<SettingsIcon slotIcon={false} />
</span>
</MediaPlaybackRateMenuButton>
<div className="settings-menu-wrap">
<MediaPlaybackRateMenu hidden />
</div>
</div>
</div>
{!isYouTube && (
<MediaPipButton>
<PipIcon />
</MediaPipButton>
)}

<MediaFullscreenButton>
<FullscreenIcon />
</MediaFullscreenButton>
</MediaControlBar>
</MediaController>
);
}

export function VideoPlayer(props: VideoPlayerProps) {
return (
<ReactPlayer
height="100%"
playing={isActive}
ref={playerRef}
src={src}
style={{ maxHeight: "80vh", margin: "auto", width: "100%" }}
onError={onError}
/>
<MediaProvider>
<VideoPlayerInner {...props} />
</MediaProvider>
);
};
}
8 changes: 6 additions & 2 deletions src/components/lightbox/LightboxRenderers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,12 @@ export const PDFSlide = ({ bookmark }: SlideProps) => (
* Renders a YouTube video slide
*/
export const YouTubeSlide = ({ bookmark, isActive }: SlideProps) => (
<div className="relative flex h-full max-h-[80vh] w-full max-w-[min(1200px,90vw)] items-end justify-center">
<VideoPlayer isActive={isActive ?? false} src={bookmark?.url ?? ""} />
<div className="relative flex h-full max-h-[80vh] w-full max-w-[min(1200px,90vw)] items-center justify-center">
<VideoPlayer
isActive={isActive ?? false}
isYouTube
src={bookmark?.url ?? ""}
/>
</div>
);

Expand Down
149 changes: 149 additions & 0 deletions src/components/video-player-icons.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { Icon } from "@/components/ui/recollect/icon";

export function PlayPauseIcon() {
return (
<Icon slot="icon" viewBox="0 0 24 24">
<g className="icon-state icon-play">
<path
d="M8.04074 2.32259C6.70797 1.51044 5 2.46975 5 4.03047V19.9696C5 21.5304 6.70798 22.4897 8.04074 21.6775L21.119 13.7079C22.398 12.9285 22.398 11.0716 21.119 10.2922L8.04074 2.32259Z"
fill="currentColor"
/>
</g>
<g className="icon-state icon-pause">
<path
d="M4 5C4 3.89543 4.89543 3 6 3H8C9.10457 3 10 3.89543 10 5V19C10 20.1046 9.10457 21 8 21H6C4.89543 21 4 20.1046 4 19V5Z"
fill="currentColor"
/>
<path
d="M14 5C14 3.89543 14.8954 3 16 3H18C19.1046 3 20 3.89543 20 5V19C20 20.1046 19.1046 21 18 21H16C14.8954 21 14 20.1046 14 19V5Z"
fill="currentColor"
/>
</g>
</Icon>
);
}

function VolumeOnIcon() {
return (
<g className="icon-state vol-on">
<path
d="M12.4719 3.11833C12.797 3.29235 13 3.63121 13 4V20C13 20.3688 12.797 20.7077 12.4719 20.8817C12.1467 21.0557 11.7522 21.0366 11.4453 20.8321L5.69722 17H3C1.89543 17 1 16.1046 1 15V9C1 7.89543 1.89543 7 3 7H5.69722L11.4453 3.16795C11.7522 2.96338 12.1467 2.94431 12.4719 3.11833Z"
fill="currentColor"
/>
<path
d="M16.5963 7.40422C16.2057 7.0137 15.5726 7.0137 15.1821 7.40422C14.7915 7.79475 14.7915 8.42791 15.1821 8.81843C15.9975 9.63387 16.5001 10.7575 16.5001 12.0004C16.5001 13.2433 15.9975 14.367 15.1821 15.1824C14.7915 15.5729 14.7915 16.2061 15.1821 16.5966C15.5726 16.9871 16.2057 16.9871 16.5963 16.5966C17.7714 15.4214 18.5001 13.7951 18.5001 12.0004C18.5001 10.2058 17.7714 8.5794 16.5963 7.40422Z"
fill="currentColor"
/>
</g>
);
}

function VolumeOffIcon() {
return (
<g className="icon-state vol-off">
<path
d="M20.7071 4.70711C21.0976 4.31658 21.0976 3.68342 20.7071 3.29289C20.3166 2.90237 19.6834 2.90237 19.2929 3.29289L17 5.58579V4C17 3.63121 16.797 3.29235 16.4719 3.11833C16.1467 2.94431 15.7522 2.96338 15.4453 3.16795L9.69722 7H7C5.89543 7 5 7.89543 5 9V15C5 15.7021 5.36182 16.3198 5.90917 16.6766L3.29289 19.2929C2.90237 19.6834 2.90237 20.3166 3.29289 20.7071C3.68342 21.0976 4.31658 21.0976 4.70711 20.7071L20.7071 4.70711Z"
fill="currentColor"
/>
<path
d="M10.6246 17.6183L17 11.2429V20C17 20.3688 16.797 20.7077 16.4719 20.8817C16.1467 21.0557 15.7522 21.0366 15.4453 20.8321L10.6246 17.6183Z"
fill="currentColor"
/>
</g>
);
}

export function MuteIcon() {
return (
<Icon slot="icon" viewBox="0 0 24 24">
<VolumeOnIcon />
<VolumeOffIcon />
</Icon>
);
}

export function SettingsIcon(props?: { slotIcon?: boolean }) {
return (
<Icon
{...(props?.slotIcon !== false && { slot: "icon" })}
viewBox="0 0 24 24"
>
<path
d="M12 2C11.4477 2 11 2.44772 11 3V5C11 5.55228 11.4477 6 12 6C12.5523 6 13 5.55228 13 5V4.06189C16.9463 4.55399 20 7.92038 20 12C20 16.4183 16.4183 20 12 20C7.58172 20 4 16.4183 4 12C4 11.0632 4.16061 10.166 4.45496 9.33325C4.639 8.81253 4.36608 8.24121 3.84536 8.05716C3.32465 7.87311 2.75332 8.14604 2.56928 8.66675C2.20032 9.71063 2 10.833 2 12C2 17.5228 6.47715 22 12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2Z"
fill="currentColor"
/>
<path
d="M5.04289 5.04289C5.43342 4.65237 6.06658 4.65237 6.45711 5.04289L10.3349 8.92069C10.8301 8.65237 11.3972 8.5 12 8.5C13.933 8.5 15.5 10.067 15.5 12C15.5 13.933 13.933 15.5 12 15.5C10.067 15.5 8.5 13.933 8.5 12C8.5 11.3972 8.65237 10.8301 8.92069 10.3349L5.04289 6.45711C4.65237 6.06658 4.65237 5.43342 5.04289 5.04289Z"
fill="currentColor"
/>
</Icon>
);
}

export function PipIcon() {
return (
<Icon slot="icon" viewBox="0 0 24 24">
<path
d="M4 4C2.89543 4 2 4.89543 2 6V16C2 17.1046 2.89543 18 4 18H9C9.55228 18 10 17.5523 10 17C10 16.4477 9.55228 16 9 16H4V6L18 6V11C18 11.5523 18.4477 12 19 12C19.5523 12 20 11.5523 20 11V6C20 4.89543 19.1046 4 18 4H4Z"
fill="currentColor"
/>
<path
d="M13.5 14C12.6716 14 12 14.6716 12 15.5V19.5C12 20.3284 12.6716 21 13.5 21H20.5C21.3284 21 22 20.3284 22 19.5V15.5C22 14.6716 21.3284 14 20.5 14H13.5Z"
fill="currentColor"
/>
</Icon>
);
}

function FullscreenEnterIcon() {
return (
<g className="icon-state fs-enter">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M3 5C3 3.89543 3.89543 3 5 3H8C8.55228 3 9 3.44772 9 4C9 4.55228 8.55228 5 8 5H5V8C5 8.55228 4.55228 9 4 9C3.44772 9 3 8.55228 3 8V5ZM15 4C15 3.44772 15.4477 3 16 3H19C20.1046 3 21 3.89543 21 5V8C21 8.55228 20.5523 9 20 9C19.4477 9 19 8.55228 19 8V5H16C15.4477 5 15 4.55228 15 4ZM4 15C4.55228 15 5 15.4477 5 16V19H8C8.55228 19 9 19.4477 9 20C9 20.5523 8.55228 21 8 21H5C3.89543 21 3 20.1046 3 19V16C3 15.4477 3.44772 15 4 15ZM20 15C20.5523 15 21 15.4477 21 16V19C21 20.1046 20.1046 21 19 21H16C15.4477 21 15 20.5523 15 20C15 19.4477 15.4477 19 16 19H19V16C19 15.4477 19.4477 15 20 15Z"
fill="currentColor"
/>
</g>
);
}

function FullscreenExitIcon() {
return (
<g className="icon-state fs-exit">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10 8C10 9.10457 9.10457 10 8 10H4C3.44772 10 3 9.55228 3 9C3 8.44772 3.44772 8 4 8H8V4C8 3.44772 8.44772 3 9 3C9.55228 3 10 3.44772 10 4V8Z"
fill="currentColor"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M21 9C21 9.55228 20.5523 10 20 10H16C14.8954 10 14 9.10457 14 8V4C14 3.44772 14.4477 3 15 3C15.5523 3 16 3.44772 16 4V8H20C20.5523 8 21 8.44772 21 9Z"
fill="currentColor"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M15 21C14.4477 21 14 20.5523 14 20V16C14 14.8954 14.8954 14 16 14H20C20.5523 14 21 14.4477 21 15C21 15.5523 20.5523 16 20 16H16V20C16 20.5523 15.5523 21 15 21Z"
fill="currentColor"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M9 21C8.44772 21 8 20.5523 8 20V16H4C3.44772 16 3 15.5523 3 15C3 14.4477 3.44772 14 4 14H8C9.10457 14 10 14.8954 10 16V20C10 20.5523 9.55228 21 9 21Z"
fill="currentColor"
/>
</g>
);
}

export function FullscreenIcon() {
return (
<Icon slot="icon" viewBox="0 0 24 24">
<FullscreenEnterIcon />
<FullscreenExitIcon />
</Icon>
);
}
Loading
Loading