Skip to content

feat: allow external control #118

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
53 changes: 47 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ npm install react-lite-youtube-embed -S
import React from "react";
import { render } from "react-dom";
import LiteYouTubeEmbed from 'react-lite-youtube-embed';
import 'react-lite-youtube-embed/dist/LiteYouTubeEmbed.css'
import 'react-lite-youtube-embed/dist/LiteYouTubeEmbed.css';

const App = () => (
<div>
Expand Down Expand Up @@ -68,8 +68,8 @@ const App = () => (
playlistCoverId="L2vS_050c-M" // The ids for playlists did not bring the cover in a pattern to render so you'll need pick up a video from the playlist (or in fact, whatever id) and use to render the cover. There's a programmatic way to get the cover from YouTube API v3 but the aim of this component is do not make any another call and reduce requests and bandwidth usage as much as possibe
poster="hqdefault" // Defines the image size to call on first render as poster image. Possible values are "default","mqdefault", "hqdefault", "sddefault" and "maxresdefault". Default value for this prop is "hqdefault". Please be aware that "sddefault" and "maxresdefault", high resolution images are not always avaialble for every video. See: https://stackoverflow.com/questions/2068344/how-do-i-get-a-youtube-video-thumbnail-from-the-youtube-api
title="YouTube Embed" // a11y, always provide a title for iFrames: https://dequeuniversity.com/tips/provide-iframe-titles Help the web be accessible ;)
noCookie={true} // Default false, connect to YouTube via the Privacy-Enhanced Mode using https://www.youtube-nocookie.com
ref={myRef} // Use this ref prop to programmatically access the underlying iframe element
cookie={false} // Default false, don't connect to YouTube via the Privacy-Enhanced Mode using https://www.youtube-nocookie.com
ref={myRef} // Use this ref prop to programmatically access the underlying iframe element. It will only have a value after the user pressed the play button
/>
</div>
);
Expand All @@ -93,6 +93,45 @@ const App = () => (
);
```

## 🤖 Controlling the player

You can programmatically control the YouTube player via [YouTubes IFrame Player API](https://developers.google.com/youtube/iframe_api_reference). However typically YouTube requires you to load an additional script from their servers (`https://www.youtube.com/iframe_api`), which is small but it will load another script. So this is neither performant nor very privacy-friendly. Instead, you can also send messages to the iframe via (`postMessage`)[https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage] using the ref prop. If you don't want to create the `postMessage()` calls yourself, there is also a little (wrapper library)[https://github.com/mich418/youtube-iframe-ctrl] for controlling the iframe with this method.

> [!WARNING]
> This will only work if you set the `enableJsApi` prop to true. Also, the ref will only be defined, when the iframe has been loaded (which happens after clicking on the poster). So you can't start the player through this method. If you really want the player to always load the iframe right away (which is not good in terms of privacy), you can use the `alwaysLoadIframe` prop to do this.

```jsx
const App = () => (
const ytRef = useRef(null);
const [isPlaying, setIsPlaying] = useState(false);

return (
<div>
<button
onClick={() => {
setIsPlaying((oldState) => !oldState);
ytRef.current?.contentWindow?.postMessage(
`{"event": "command", "func": "${isPlaying ? "pauseVideo" : "playVideo"}"}`,
"*",
);
}}
>
External Play Button
</button>
<YouTubeNew
title="My Video"
id="L2vS_050c-M"
ref={ytRef}
enableJsApi
alwaysLoadIframe
/>
</div>
);
};
);

```

## ⚠️ After version 1.0.0 - BREAKING CHANGES ⚠️

To play nice with new frameworks like [NextJS](https://nextjs.org/), we now don't import the `.css` necessary. Since version `2.0.9` you can pass custom aspect-ratio props, so be aware of any changes needed in the CSS options. Instead use now you have three options:
Expand Down Expand Up @@ -222,13 +261,13 @@ Not work on every framework but you can import the css directly, check what work
<summary>Show me the code!</summary>

```ts
import 'react-lite-youtube-embed/dist/LiteYouTubeEmbed.css';
import 'react-lite-youtube-embed/dist/index.css';
```

or in a *.css/scss etc:

```css
@import "~react-lite-youtube-embed/dist/LiteYouTubeEmbed.css";
@import "~react-lite-youtube-embed/dist/index.css";
```

</details>
Expand All @@ -246,7 +285,9 @@ The most minimalist implementation requires two props: `id` from the YouTube you
| announce | string | Default: `Watch`. This will added to the button announce to the final user as in `Clickable Watch, ${title}, button` , customize to match your own language #a11y #i18n |
| aspectHeight | number | Default: `9`. Use this optional prop if you want a custom aspect-ratio. Please be aware of aspect height and width relation and also any custom CSS you are using. |
| aspectWidth | number | Default: `16`. Use this optional prop if you want a custom aspect-ratio. Please be aware of aspect height and width relation and also any custom CSS you are using. |
| cookie | boolean | Default: `false` Connect to YouTube via the Privacy-Enhanced Mode using [https://www.youtube-nocookie.com](https://www.youtube-nocookie.com). You should opt-in to allow cookies|
| cookie | boolean | Default: `false`. Connect to YouTube via the Privacy-Enhanced Mode using [https://www.youtube-nocookie.com](https://www.youtube-nocookie.com). You should opt-in to allow cookies|
| enableJsApi | boolean | Default: `false`. If this is enabled, you can send messages to the iframe (e.g. access via the `ref` prop) to control the player programmatically. |
| alwaysLoadIframe | boolean | Default: `false`. If this is enabled, the original YouTube iframe will always be loaded right away (this is bad for privacy). |
| iframeClass | string | Pass the string class for the own iFrame |
| muted | boolean | If the video has sound or not. Required autoplay `true` to work |
| noCookie | boolean | `Deprecated` Default `false` _use option **cookie** to opt-in_|
Expand Down
73 changes: 45 additions & 28 deletions src/lib/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,36 +18,46 @@ export interface LiteYouTubeProps {
iframeClass?: string;
noCookie?: boolean;
cookie?: boolean;
enableJsApi?: boolean;
alwaysLoadIframe?: boolean;
params?: string;
playerClass?: string;
playlist?: boolean;
playlistCoverId?: string;
poster?: imgResolution;
webp?: boolean;
wrapperClass?: string;
onIframeAdded?: () => void
muted?: boolean,
thumbnail?: string,
rel?: string,
containerElement?: keyof JSX.IntrinsicElements;
onIframeAdded?: () => void;
muted?: boolean;
thumbnail?: string;
rel?: string;
containerElement?: keyof React.JSX.IntrinsicElements;
style?: React.CSSProperties;
}

function LiteYouTubeEmbedComponent(props: LiteYouTubeProps, ref: React.Ref<HTMLIFrameElement>) {
function LiteYouTubeEmbedComponent(
props: LiteYouTubeProps,
ref: React.Ref<HTMLIFrameElement>,
) {
const [preconnected, setPreconnected] = React.useState(false);
const [iframe, setIframe] = React.useState(false);
const [iframe, setIframe] = React.useState(props.alwaysLoadIframe || false);
const videoId = encodeURIComponent(props.id);
const videoPlaylisCovertId = typeof props.playlistCoverId === 'string' ? encodeURIComponent(props.playlistCoverId) : null;
const videoPlaylistCoverId =
typeof props.playlistCoverId === "string"
? encodeURIComponent(props.playlistCoverId)
: null;
const videoTitle = props.title;
const posterImp = props.poster || "hqdefault";
const paramsImp = `&${props.params}` || "";
const mutedImp = props.muted ? "&mute=1" : "";
const announceWatch = props.announce || "Watch";
const format = props.webp ? 'webp' : 'jpg';
const vi = props.webp ? 'vi_webp' : 'vi';
const posterUrl = props.thumbnail || (!props.playlist
? `https://i.ytimg.com/${vi}/${videoId}/${posterImp}.${format}`
: `https://i.ytimg.com/${vi}/${videoPlaylisCovertId}/${posterImp}.${format}`);

// Iframe Parameters
const iframeParams = new URLSearchParams({
...(props.muted ? { mute: "1" } : {}),
// When the iframe is not loaded immediately, the video should play as soon as its loaded (which happens when the button is clicked)
...(props.alwaysLoadIframe ? {} : { autoplay: "1", state: "1" }),
...(props.enableJsApi ? { enablejsapi: "1" } : {}),
...(props.playlist ? { list: videoId } : {}),
});

let ytUrl = props.noCookie
? "https://www.youtube-nocookie.com"
Expand All @@ -57,8 +67,16 @@ function LiteYouTubeEmbedComponent(props: LiteYouTubeProps, ref: React.Ref<HTMLI
: "https://www.youtube-nocookie.com";

const iframeSrc = !props.playlist
? `${ytUrl}/embed/${videoId}?autoplay=1&state=1${mutedImp}${paramsImp}`
: `${ytUrl}/embed/videoseries?autoplay=1${mutedImp}&list=${videoId}${paramsImp}`;
? `${ytUrl}/embed/${videoId}?${iframeParams.toString()}`
: `${ytUrl}/embed/videoseries?${iframeParams.toString()}`;

const format = props.webp ? "webp" : "jpg";
const vi = props.webp ? "vi_webp" : "vi";
const posterUrl =
props.thumbnail ||
(!props.playlist
? `https://i.ytimg.com/${vi}/${videoId}/${posterImp}.${format}`
: `https://i.ytimg.com/${vi}/${videoPlaylistCoverId}/${posterImp}.${format}`);

const activatedClassImp = props.activatedClass || "lyt-activated";
const adNetworkImp = props.adNetwork || false;
Expand All @@ -67,9 +85,9 @@ function LiteYouTubeEmbedComponent(props: LiteYouTubeProps, ref: React.Ref<HTMLI
const iframeClassImp = props.iframeClass || "";
const playerClassImp = props.playerClass || "lty-playbtn";
const wrapperClassImp = props.wrapperClass || "yt-lite";
const onIframeAdded = props.onIframeAdded || function () { };
const rel = props.rel ? 'prefetch' : 'preload';
const ContainerElement = props.containerElement || 'article';
const onIframeAdded = props.onIframeAdded || function () {};
const rel = props.rel ? "prefetch" : "preload";
const ContainerElement = props.containerElement || "article";
const style = props.style || {};

const warmConnections = () => {
Expand All @@ -90,11 +108,7 @@ function LiteYouTubeEmbedComponent(props: LiteYouTubeProps, ref: React.Ref<HTMLI

return (
<>
<link
rel={rel}
href={posterUrl}
as="image"
/>
<link rel={rel} href={posterUrl} as="image" />
<>
{preconnected && (
<>
Expand All @@ -120,15 +134,16 @@ function LiteYouTubeEmbedComponent(props: LiteYouTubeProps, ref: React.Ref<HTMLI
style={{
backgroundImage: `url(${posterUrl})`,
...({
'--aspect-ratio': `${(aspectHeight / aspectWidth) * 100}%`,
"--aspect-ratio": `${(aspectHeight / aspectWidth) * 100}%`,
} as React.CSSProperties),
...style,
}}
>
<button
type="button"
className={playerClassImp}
aria-label={`${announceWatch} ${videoTitle}`} />
aria-label={`${announceWatch} ${videoTitle}`}
/>
{iframe && (
<iframe
ref={ref}
Expand All @@ -147,4 +162,6 @@ function LiteYouTubeEmbedComponent(props: LiteYouTubeProps, ref: React.Ref<HTMLI
);
}

export default React.forwardRef<HTMLIFrameElement, LiteYouTubeProps>(LiteYouTubeEmbedComponent)
export default React.forwardRef<HTMLIFrameElement, LiteYouTubeProps>(
LiteYouTubeEmbedComponent,
);