Plugin

PictureInPicture

Adds picture in picture to videos (next to the Download button)

Media Utility
index.tsx
Download

Source

src/plugins/pictureInPicture/index.tsx
1/*
2 * FiveCord — a Discord client mod
3 * Copyright (c) 2025 FiveCord
4 * SPDX-License-Identifier: GPL-3.0-or-later
5 */
6
7import "./styles.css";
8
9import { definePluginSettings } from "@api/Settings";
10import ErrorBoundary from "@components/ErrorBoundary";
11import { Devs } from "@utils/constants";
12import definePlugin, { OptionType } from "@utils/types";
13import { Tooltip } from "@webpack/common";
14
15const settings = definePluginSettings({
16 loop: {
17 description: "Whether to make the PiP video loop or not",
18 type: OptionType.BOOLEAN,
19 default: true,
20 restartNeeded: false
21 }
22});
23
24export default definePlugin({
25 name: "PictureInPicture",
26 description: "Adds picture in picture to videos (next to the Download button)",
27 tags: ["Media", "Utility"],
28 authors: [Devs.Lumap],
29 settings,
30 patches: [
31 {
32 find: '["VIDEO","CLIP","AUDIO"]',
33 replacement: {
34 match: /(\[\i>0&&\i\.length>0.{0,150}?children:)(\i.slice\(\i\))(?<=showDownload:(\i).+?isVisualMediaType:(\i).+?)/,
35 replace: (_, rest, origChildren, showDownload, isVisualMediaType) => `${rest}[${showDownload}&&${isVisualMediaType}&&$self.PictureInPictureButton(),...${origChildren}]`
36 }
37 }
38 ],
39
40 PictureInPictureButton: ErrorBoundary.wrap(() => {
41 return (
42 <Tooltip text="Toggle Picture in Picture">
43 {tooltipProps => (
44 <div
45 {...tooltipProps}
46 className="vc-pip-button"
47 role="button"
48 style={{
49 cursor: "pointer",
50 paddingTop: "4px",
51 paddingLeft: "4px",
52 paddingRight: "4px",
53 }}
54 onClick={e => {
55 const video = e.currentTarget.parentNode!.parentNode!.querySelector("video")!;
56 const videoClone = document.body.appendChild(video.cloneNode(true)) as HTMLVideoElement;
57
58 videoClone.loop = settings.store.loop;
59 videoClone.style.display = "none";
60 videoClone.onleavepictureinpicture = () => videoClone.remove();
61
62 function launchPiP() {
63 videoClone.currentTime = video.currentTime;
64 videoClone.requestPictureInPicture();
65 video.pause();
66 videoClone.play();
67 }
68
69 if (videoClone.readyState === 4 /* HAVE_ENOUGH_DATA */)
70 launchPiP();
71 else
72 videoClone.onloadedmetadata = launchPiP;
73 }}
74 >
75 <svg width="24px" height="24px" viewBox="0 0 24 24">
76 <path
77 fill="currentColor"
78 d="M21 3a1 1 0 0 1 1 1v7h-2V5H4v14h6v2H3a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h18zm0 10a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1h-8a1 1 0 0 1-1-1v-6a1 1 0 0 1 1-1h8zm-1 2h-6v4h6v-4z"
79 />
80 </svg>
81 </div>
82 )}
83 </Tooltip>
84 );
85 }, { noop: true })
86});
87