@cntrl-site/sdk-nextjs 0.13.0 → 0.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cntrl-site-sdk-nextjs-0.12.9.tgz +0 -0
- package/lib/components/items/VimeoEmbed.js +5 -13
- package/lib/components/items/YoutubeEmbed.js +44 -13
- package/lib/utils/Youtube/YouTubeIframeApiLoader.js +53 -0
- package/lib/utils/Youtube/YoutubeIframeApi.js +12 -0
- package/lib/utils/Youtube/useYouTubeIframeApi.js +14 -0
- package/package.json +3 -3
- package/src/components/items/VimeoEmbed.tsx +5 -11
- package/src/components/items/YoutubeEmbed.tsx +45 -21
- package/src/utils/Youtube/YouTubeIframeApiLoader.ts +58 -0
- package/src/utils/Youtube/YoutubeIframeApi.ts +101 -0
- package/src/utils/Youtube/useYouTubeIframeApi.ts +12 -0
|
Binary file
|
|
@@ -14,27 +14,19 @@ const useItemAngle_1 = require("../useItemAngle");
|
|
|
14
14
|
const sdk_1 = require("@cntrl-site/sdk");
|
|
15
15
|
const useCntrlContext_1 = require("../../provider/useCntrlContext");
|
|
16
16
|
const HoverStyles_1 = require("../../utils/HoverStyles/HoverStyles");
|
|
17
|
-
const useCurrentLayout_1 = require("../../common/useCurrentLayout");
|
|
18
17
|
const VimeoEmbedItem = ({ item, sectionId }) => {
|
|
19
18
|
const id = (0, react_1.useId)();
|
|
20
19
|
const { layouts } = (0, useCntrlContext_1.useCntrlContext)();
|
|
21
|
-
const layout = (0, useCurrentLayout_1.useCurrentLayout)();
|
|
22
20
|
const { radius } = (0, useEmbedVideoItem_1.useEmbedVideoItem)(item, sectionId);
|
|
23
21
|
const [iframeRef, setIframeRef] = (0, react_1.useState)(null);
|
|
24
22
|
const vimeoPlayer = (0, react_1.useMemo)(() => iframeRef ? new player_1.default(iframeRef) : undefined, [iframeRef]);
|
|
25
|
-
const isAutoPlayOnHover = (0, react_1.useMemo)(() => {
|
|
26
|
-
const layoutHoverStates = item.state.hover[layout];
|
|
27
|
-
if (!layoutHoverStates)
|
|
28
|
-
return false;
|
|
29
|
-
return layoutHoverStates.autoplay === true;
|
|
30
|
-
}, [layout, item]);
|
|
31
23
|
const angle = (0, useItemAngle_1.useItemAngle)(item, sectionId);
|
|
32
|
-
const {
|
|
24
|
+
const { play, controls, loop, muted, pictureInPicture, url } = item.commonParams;
|
|
33
25
|
const getValidVimeoUrl = (url) => {
|
|
34
26
|
const validURL = new URL(url);
|
|
35
27
|
validURL.searchParams.append('controls', String(controls));
|
|
36
|
-
validURL.searchParams.append('autoplay', String(
|
|
37
|
-
validURL.searchParams.append('muted', String(muted));
|
|
28
|
+
validURL.searchParams.append('autoplay', String(play === 'auto'));
|
|
29
|
+
validURL.searchParams.append('muted', String(play !== 'on-click' ? true : muted));
|
|
38
30
|
validURL.searchParams.append('loop', String(loop));
|
|
39
31
|
validURL.searchParams.append('pip', String(pictureInPicture));
|
|
40
32
|
validURL.searchParams.append('title', '0');
|
|
@@ -48,11 +40,11 @@ const VimeoEmbedItem = ({ item, sectionId }) => {
|
|
|
48
40
|
borderRadius: `${radius * 100}vw`,
|
|
49
41
|
transform: `rotate(${angle}deg)`
|
|
50
42
|
}, onMouseEnter: () => {
|
|
51
|
-
if (!vimeoPlayer ||
|
|
43
|
+
if (!vimeoPlayer || play !== 'on-hover')
|
|
52
44
|
return;
|
|
53
45
|
vimeoPlayer.play();
|
|
54
46
|
}, onMouseLeave: () => {
|
|
55
|
-
if (!vimeoPlayer ||
|
|
47
|
+
if (!vimeoPlayer || play !== 'on-hover')
|
|
56
48
|
return;
|
|
57
49
|
vimeoPlayer.pause();
|
|
58
50
|
}, children: (0, jsx_runtime_1.jsx)("iframe", { ref: setIframeRef, className: "embedVideo", src: validUrl || '', allow: "autoplay; fullscreen; picture-in-picture;", allowFullScreen: true }) }), (0, jsx_runtime_1.jsx)(style_1.default, { id: id, children: `
|
|
@@ -14,33 +14,64 @@ const useItemAngle_1 = require("../useItemAngle");
|
|
|
14
14
|
const sdk_1 = require("@cntrl-site/sdk");
|
|
15
15
|
const HoverStyles_1 = require("../../utils/HoverStyles/HoverStyles");
|
|
16
16
|
const useCntrlContext_1 = require("../../provider/useCntrlContext");
|
|
17
|
+
const useYouTubeIframeApi_1 = require("../../utils/Youtube/useYouTubeIframeApi");
|
|
17
18
|
const YoutubeEmbedItem = ({ item, sectionId }) => {
|
|
18
19
|
const id = (0, react_1.useId)();
|
|
19
20
|
const { layouts } = (0, useCntrlContext_1.useCntrlContext)();
|
|
20
|
-
const {
|
|
21
|
+
const { play, controls, url } = item.commonParams;
|
|
21
22
|
const { radius } = (0, useEmbedVideoItem_1.useEmbedVideoItem)(item, sectionId);
|
|
22
23
|
const angle = (0, useItemAngle_1.useItemAngle)(item, sectionId);
|
|
23
|
-
const
|
|
24
|
+
const YT = (0, useYouTubeIframeApi_1.useYouTubeIframeApi)();
|
|
25
|
+
const [div, setDiv] = (0, react_1.useState)(null);
|
|
26
|
+
const [player, setPlayer] = (0, react_1.useState)(undefined);
|
|
27
|
+
(0, react_1.useEffect)(() => {
|
|
24
28
|
const newUrl = new URL(url);
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
const videoId = (0, getValidYoutubeUrl_1.getYoutubeId)(newUrl);
|
|
30
|
+
if (!YT || !videoId || !div)
|
|
31
|
+
return;
|
|
32
|
+
const divRect = div.getBoundingClientRect();
|
|
33
|
+
const placeholder = document.createElement('div');
|
|
34
|
+
div.appendChild(placeholder);
|
|
35
|
+
const player = new YT.Player(placeholder, {
|
|
36
|
+
videoId,
|
|
37
|
+
playerVars: {
|
|
38
|
+
autoplay: play === 'auto' ? '1' : '0',
|
|
39
|
+
controls: controls ? '1' : '0'
|
|
40
|
+
},
|
|
41
|
+
events: {
|
|
42
|
+
onReady: (event) => {
|
|
43
|
+
setPlayer(event.target);
|
|
44
|
+
if (play !== 'on-click') {
|
|
45
|
+
player.mute();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
return () => {
|
|
51
|
+
setPlayer(undefined);
|
|
52
|
+
player.destroy();
|
|
53
|
+
placeholder.parentElement?.removeChild(placeholder);
|
|
54
|
+
};
|
|
55
|
+
}, [YT, div]);
|
|
56
|
+
return ((0, jsx_runtime_1.jsxs)(LinkWrapper_1.LinkWrapper, { url: item.link?.url, children: [(0, jsx_runtime_1.jsx)("div", { className: `embed-youtube-video-wrapper-${item.id}`, onMouseEnter: () => {
|
|
57
|
+
if (!player || play !== 'on-hover')
|
|
58
|
+
return;
|
|
59
|
+
player.playVideo();
|
|
60
|
+
}, onMouseLeave: () => {
|
|
61
|
+
if (!player || play !== 'on-hover')
|
|
62
|
+
return;
|
|
63
|
+
player.pauseVideo();
|
|
64
|
+
}, ref: setDiv, style: {
|
|
34
65
|
borderRadius: `${radius * 100}vw`,
|
|
35
66
|
transform: `rotate(${angle}deg)`
|
|
36
|
-
}
|
|
67
|
+
} }), (0, jsx_runtime_1.jsx)(style_1.default, { id: id, children: `
|
|
37
68
|
.embed-youtube-video-wrapper-${item.id} {
|
|
38
69
|
position: absolute;
|
|
39
70
|
overflow: hidden;
|
|
40
71
|
width: 100%;
|
|
41
72
|
height: 100%;
|
|
42
73
|
}
|
|
43
|
-
.
|
|
74
|
+
.embed-youtube-video-wrapper-${item.id} iframe {
|
|
44
75
|
width: 100%;
|
|
45
76
|
height: 100%;
|
|
46
77
|
z-index: 1;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.captureStackTrace = exports.YouTubeIframeApiLoader = void 0;
|
|
4
|
+
const YT_IFRAME_API_URL = 'https://www.youtube.com/iframe_api';
|
|
5
|
+
class YouTubeIframeApiLoader {
|
|
6
|
+
static create() {
|
|
7
|
+
if (!YouTubeIframeApiLoader.instance) {
|
|
8
|
+
YouTubeIframeApiLoader.instance = new YouTubeIframeApiLoader();
|
|
9
|
+
}
|
|
10
|
+
return YouTubeIframeApiLoader.instance;
|
|
11
|
+
}
|
|
12
|
+
constructor() {
|
|
13
|
+
this.loadingPromise = this.loadApi();
|
|
14
|
+
}
|
|
15
|
+
async getApi() {
|
|
16
|
+
await this.loadingPromise;
|
|
17
|
+
if (!window.YT) {
|
|
18
|
+
throw new Error('YouTube IFrame API is not loaded');
|
|
19
|
+
}
|
|
20
|
+
return window.YT;
|
|
21
|
+
}
|
|
22
|
+
loadApi() {
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
const script = document.createElement('script');
|
|
25
|
+
script.src = YT_IFRAME_API_URL;
|
|
26
|
+
const loadError = new Error('YouTube IFrame API loading failed');
|
|
27
|
+
captureStackTrace(loadError);
|
|
28
|
+
script.addEventListener('abort', () => {
|
|
29
|
+
reject(loadError);
|
|
30
|
+
});
|
|
31
|
+
script.addEventListener('error', () => {
|
|
32
|
+
reject(loadError);
|
|
33
|
+
});
|
|
34
|
+
window.onYouTubeIframeAPIReady = () => {
|
|
35
|
+
window.onYouTubeIframeAPIReady = undefined;
|
|
36
|
+
resolve();
|
|
37
|
+
};
|
|
38
|
+
document.head.appendChild(script);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
exports.YouTubeIframeApiLoader = YouTubeIframeApiLoader;
|
|
43
|
+
function captureStackTrace(error) {
|
|
44
|
+
if (typeof Error.captureStackTrace === 'function') {
|
|
45
|
+
Error.captureStackTrace(error);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
catch (errWithStack) { }
|
|
52
|
+
}
|
|
53
|
+
exports.captureStackTrace = captureStackTrace;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.YTPlayerState = void 0;
|
|
4
|
+
var YTPlayerState;
|
|
5
|
+
(function (YTPlayerState) {
|
|
6
|
+
YTPlayerState[YTPlayerState["Unstarted"] = -1] = "Unstarted";
|
|
7
|
+
YTPlayerState[YTPlayerState["Ended"] = 0] = "Ended";
|
|
8
|
+
YTPlayerState[YTPlayerState["Playing"] = 1] = "Playing";
|
|
9
|
+
YTPlayerState[YTPlayerState["Paused"] = 2] = "Paused";
|
|
10
|
+
YTPlayerState[YTPlayerState["Buffering"] = 3] = "Buffering";
|
|
11
|
+
YTPlayerState[YTPlayerState["VideoCued"] = 5] = "VideoCued";
|
|
12
|
+
})(YTPlayerState = exports.YTPlayerState || (exports.YTPlayerState = {}));
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useYouTubeIframeApi = void 0;
|
|
4
|
+
const react_1 = require("react");
|
|
5
|
+
const YouTubeIframeApiLoader_1 = require("./YouTubeIframeApiLoader");
|
|
6
|
+
function useYouTubeIframeApi() {
|
|
7
|
+
const [YT, setYT] = (0, react_1.useState)(undefined);
|
|
8
|
+
(0, react_1.useEffect)(() => {
|
|
9
|
+
const loader = YouTubeIframeApiLoader_1.YouTubeIframeApiLoader.create();
|
|
10
|
+
loader.getApi().then((YT) => setYT(YT));
|
|
11
|
+
}, []);
|
|
12
|
+
return YT;
|
|
13
|
+
}
|
|
14
|
+
exports.useYouTubeIframeApi = useYouTubeIframeApi;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cntrl-site/sdk-nextjs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.1",
|
|
4
4
|
"description": "SDK for Next.js",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
},
|
|
22
22
|
"homepage": "https://github.com/cntrl-site/sdk-nextjs#readme",
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@
|
|
24
|
+
"@types/vimeo__player": "^2.18.0",
|
|
25
|
+
"@cntrl-site/sdk": "^1.1.8",
|
|
25
26
|
"@vimeo/player": "^2.20.1",
|
|
26
27
|
"html-react-parser": "^3.0.1",
|
|
27
28
|
"styled-jsx": "^5.0.2"
|
|
@@ -39,7 +40,6 @@
|
|
|
39
40
|
"@types/jest": "^29.2.5",
|
|
40
41
|
"@types/node": "^18.11.7",
|
|
41
42
|
"@types/react": "^18.0.15",
|
|
42
|
-
"@types/vimeo__player": "^2.18.0",
|
|
43
43
|
"jest": "^29.3.1",
|
|
44
44
|
"ts-jest": "^29.0.3",
|
|
45
45
|
"typescript": "^4.9.5"
|
|
@@ -14,22 +14,16 @@ import { useCurrentLayout } from '../../common/useCurrentLayout';
|
|
|
14
14
|
export const VimeoEmbedItem: FC<ItemProps<TVimeoEmbedItem>> = ({ item, sectionId }) => {
|
|
15
15
|
const id = useId();
|
|
16
16
|
const { layouts } = useCntrlContext();
|
|
17
|
-
const layout = useCurrentLayout()
|
|
18
17
|
const { radius } = useEmbedVideoItem(item, sectionId);
|
|
19
18
|
const [iframeRef, setIframeRef] = useState<HTMLIFrameElement | null>(null);
|
|
20
19
|
const vimeoPlayer = useMemo(() => iframeRef ? new Player(iframeRef) : undefined, [iframeRef]);
|
|
21
|
-
const isAutoPlayOnHover = useMemo(() => {
|
|
22
|
-
const layoutHoverStates = item.state.hover[layout];
|
|
23
|
-
if (!layoutHoverStates) return false;
|
|
24
|
-
return layoutHoverStates.autoplay === true;
|
|
25
|
-
}, [layout, item]);
|
|
26
20
|
const angle = useItemAngle(item, sectionId);
|
|
27
|
-
const {
|
|
21
|
+
const { play, controls, loop, muted, pictureInPicture, url } = item.commonParams;
|
|
28
22
|
const getValidVimeoUrl = (url: string): string => {
|
|
29
23
|
const validURL = new URL(url);
|
|
30
24
|
validURL.searchParams.append('controls', String(controls));
|
|
31
|
-
validURL.searchParams.append('autoplay', String(
|
|
32
|
-
validURL.searchParams.append('muted', String(muted));
|
|
25
|
+
validURL.searchParams.append('autoplay', String(play === 'auto'));
|
|
26
|
+
validURL.searchParams.append('muted', String(play !== 'on-click' ? true : muted));
|
|
33
27
|
validURL.searchParams.append('loop', String(loop));
|
|
34
28
|
validURL.searchParams.append('pip', String(pictureInPicture));
|
|
35
29
|
validURL.searchParams.append('title', '0');
|
|
@@ -49,11 +43,11 @@ export const VimeoEmbedItem: FC<ItemProps<TVimeoEmbedItem>> = ({ item, sectionId
|
|
|
49
43
|
transform: `rotate(${angle}deg)`
|
|
50
44
|
}}
|
|
51
45
|
onMouseEnter={() => {
|
|
52
|
-
if (!vimeoPlayer ||
|
|
46
|
+
if (!vimeoPlayer || play !== 'on-hover') return;
|
|
53
47
|
vimeoPlayer.play();
|
|
54
48
|
}}
|
|
55
49
|
onMouseLeave={() =>{
|
|
56
|
-
if (!vimeoPlayer ||
|
|
50
|
+
if (!vimeoPlayer || play !== 'on-hover') return;
|
|
57
51
|
vimeoPlayer.pause();
|
|
58
52
|
}}
|
|
59
53
|
>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FC, useId } from 'react';
|
|
1
|
+
import { FC, useEffect, useId, useState } from 'react';
|
|
2
2
|
import JSXStyle from 'styled-jsx/style';
|
|
3
3
|
import { TYoutubeEmbedItem } from '@cntrl-site/core';
|
|
4
4
|
import { ItemProps } from '../Item';
|
|
@@ -9,41 +9,65 @@ import { useItemAngle } from '../useItemAngle';
|
|
|
9
9
|
import { ArticleItemType, getLayoutStyles } from '@cntrl-site/sdk';
|
|
10
10
|
import { getHoverStyles, getTransitions } from '../../utils/HoverStyles/HoverStyles';
|
|
11
11
|
import { useCntrlContext } from '../../provider/useCntrlContext';
|
|
12
|
+
import { useYouTubeIframeApi } from '../../utils/Youtube/useYouTubeIframeApi';
|
|
13
|
+
import { YTPlayer } from '../../utils/Youtube/YoutubeIframeApi';
|
|
12
14
|
|
|
13
15
|
export const YoutubeEmbedItem: FC<ItemProps<TYoutubeEmbedItem>> = ({ item, sectionId }) => {
|
|
14
16
|
const id = useId();
|
|
15
17
|
const { layouts } = useCntrlContext();
|
|
16
|
-
const {
|
|
18
|
+
const { play, controls, url } = item.commonParams;
|
|
17
19
|
const { radius } = useEmbedVideoItem(item, sectionId);
|
|
18
20
|
const angle = useItemAngle(item, sectionId);
|
|
21
|
+
const YT = useYouTubeIframeApi();
|
|
22
|
+
const [div, setDiv] = useState<HTMLDivElement | null>(null);
|
|
23
|
+
const [player, setPlayer] = useState<YTPlayer | undefined>(undefined);
|
|
19
24
|
|
|
20
|
-
|
|
25
|
+
useEffect(() => {
|
|
21
26
|
const newUrl = new URL(url);
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
const videoId = getYoutubeId(newUrl);
|
|
28
|
+
if (!YT || !videoId || !div) return;
|
|
29
|
+
const divRect = div.getBoundingClientRect();
|
|
30
|
+
const placeholder = document.createElement('div');
|
|
31
|
+
div.appendChild(placeholder);
|
|
32
|
+
const player = new YT.Player(placeholder, {
|
|
33
|
+
videoId,
|
|
34
|
+
playerVars: {
|
|
35
|
+
autoplay: play === 'auto' ? '1' : '0',
|
|
36
|
+
controls: controls ? '1' : '0'
|
|
37
|
+
},
|
|
38
|
+
events: {
|
|
39
|
+
onReady: (event) => {
|
|
40
|
+
setPlayer(event.target);
|
|
41
|
+
if (play !== 'on-click') {
|
|
42
|
+
player.mute();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
return () => {
|
|
48
|
+
setPlayer(undefined);
|
|
49
|
+
player.destroy();
|
|
50
|
+
placeholder.parentElement?.removeChild(placeholder);
|
|
51
|
+
};
|
|
52
|
+
}, [YT, div]);
|
|
31
53
|
|
|
32
54
|
return (
|
|
33
55
|
<LinkWrapper url={item.link?.url}>
|
|
34
56
|
<div className={`embed-youtube-video-wrapper-${item.id}`}
|
|
57
|
+
onMouseEnter={() => {
|
|
58
|
+
if (!player || play !== 'on-hover') return;
|
|
59
|
+
player.playVideo();
|
|
60
|
+
}}
|
|
61
|
+
onMouseLeave={() => {
|
|
62
|
+
if (!player || play !== 'on-hover') return;
|
|
63
|
+
player.pauseVideo();
|
|
64
|
+
}}
|
|
65
|
+
ref={setDiv}
|
|
35
66
|
style={{
|
|
36
67
|
borderRadius: `${radius * 100}vw`,
|
|
37
68
|
transform: `rotate(${angle}deg)`
|
|
38
69
|
}}
|
|
39
|
-
>
|
|
40
|
-
<iframe
|
|
41
|
-
className="embedYoutubeVideo"
|
|
42
|
-
src={validUrl || ''}
|
|
43
|
-
allow="accelerometer; autoplay; allowfullscreen;"
|
|
44
|
-
allowFullScreen
|
|
45
|
-
/>
|
|
46
|
-
</div>
|
|
70
|
+
></div>
|
|
47
71
|
<JSXStyle id={id}>{`
|
|
48
72
|
.embed-youtube-video-wrapper-${item.id} {
|
|
49
73
|
position: absolute;
|
|
@@ -51,7 +75,7 @@ export const YoutubeEmbedItem: FC<ItemProps<TYoutubeEmbedItem>> = ({ item, secti
|
|
|
51
75
|
width: 100%;
|
|
52
76
|
height: 100%;
|
|
53
77
|
}
|
|
54
|
-
.
|
|
78
|
+
.embed-youtube-video-wrapper-${item.id} iframe {
|
|
55
79
|
width: 100%;
|
|
56
80
|
height: 100%;
|
|
57
81
|
z-index: 1;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { YT } from './YoutubeIframeApi';
|
|
2
|
+
|
|
3
|
+
const YT_IFRAME_API_URL = 'https://www.youtube.com/iframe_api';
|
|
4
|
+
|
|
5
|
+
export class YouTubeIframeApiLoader {
|
|
6
|
+
private static instance: YouTubeIframeApiLoader | undefined;
|
|
7
|
+
|
|
8
|
+
public static create(): YouTubeIframeApiLoader {
|
|
9
|
+
if (!YouTubeIframeApiLoader.instance) {
|
|
10
|
+
YouTubeIframeApiLoader.instance = new YouTubeIframeApiLoader();
|
|
11
|
+
}
|
|
12
|
+
return YouTubeIframeApiLoader.instance;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
private loadingPromise: Promise<void>;
|
|
16
|
+
|
|
17
|
+
private constructor() {
|
|
18
|
+
this.loadingPromise = this.loadApi();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async getApi(): Promise<YT> {
|
|
22
|
+
await this.loadingPromise;
|
|
23
|
+
if (!window.YT) {
|
|
24
|
+
throw new Error('YouTube IFrame API is not loaded');
|
|
25
|
+
}
|
|
26
|
+
return window.YT;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private loadApi(): Promise<void> {
|
|
30
|
+
return new Promise<void>((resolve, reject) => {
|
|
31
|
+
const script = document.createElement('script');
|
|
32
|
+
script.src = YT_IFRAME_API_URL;
|
|
33
|
+
const loadError = new Error('YouTube IFrame API loading failed');
|
|
34
|
+
captureStackTrace(loadError);
|
|
35
|
+
script.addEventListener('abort', () => {
|
|
36
|
+
reject(loadError);
|
|
37
|
+
});
|
|
38
|
+
script.addEventListener('error', () => {
|
|
39
|
+
reject(loadError);
|
|
40
|
+
});
|
|
41
|
+
window.onYouTubeIframeAPIReady = () => {
|
|
42
|
+
window.onYouTubeIframeAPIReady = undefined;
|
|
43
|
+
resolve();
|
|
44
|
+
};
|
|
45
|
+
document.head.appendChild(script);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function captureStackTrace(error: Error): void {
|
|
51
|
+
if (typeof Error.captureStackTrace === 'function') {
|
|
52
|
+
Error.captureStackTrace(error);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
throw error;
|
|
57
|
+
} catch (errWithStack) {}
|
|
58
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
export interface YT {
|
|
2
|
+
Player: YTPlayerConstructor;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export interface YTPlayerConstructor {
|
|
6
|
+
new(container: string | Element, params: YTPlayerParams): YTPlayer;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface YTEvent<Data> {
|
|
10
|
+
target: YTPlayer;
|
|
11
|
+
data: Data;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type YTBool = 0 | 1 | '0' | '1';
|
|
15
|
+
|
|
16
|
+
export interface YTPlayerParams {
|
|
17
|
+
width?: string | number;
|
|
18
|
+
height?: string | number;
|
|
19
|
+
videoId?: string;
|
|
20
|
+
playerVars?: Partial<{
|
|
21
|
+
autoplay: YTBool;
|
|
22
|
+
cc_lang_pref: string;
|
|
23
|
+
cc_load_policy: YTBool;
|
|
24
|
+
color: string;
|
|
25
|
+
controls: YTBool;
|
|
26
|
+
disablekb: YTBool;
|
|
27
|
+
enablejsapi: YTBool;
|
|
28
|
+
end: number | string;
|
|
29
|
+
fs: YTBool;
|
|
30
|
+
hl: string;
|
|
31
|
+
iv_load_policy: 1 | 3 | '1' | '3';
|
|
32
|
+
list: string;
|
|
33
|
+
listType: 'playlist' | 'user_uploads';
|
|
34
|
+
loop: YTBool;
|
|
35
|
+
origin: string;
|
|
36
|
+
playlist: string;
|
|
37
|
+
playsinline: YTBool;
|
|
38
|
+
rel: YTBool;
|
|
39
|
+
start: number | string;
|
|
40
|
+
widget_referrer: string;
|
|
41
|
+
}>;
|
|
42
|
+
events?: Partial<{
|
|
43
|
+
onReady: (event: YTEvent<void>) => void;
|
|
44
|
+
onStateChange: (event: YTEvent<YTPlayerState>) => void;
|
|
45
|
+
}>
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export enum YTPlayerState {
|
|
49
|
+
Unstarted = -1,
|
|
50
|
+
Ended = 0,
|
|
51
|
+
Playing = 1,
|
|
52
|
+
Paused = 2,
|
|
53
|
+
Buffering = 3,
|
|
54
|
+
VideoCued = 5
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface YTPlayer {
|
|
58
|
+
loadVideoById(videoId: string, startSeconds?: number): void;
|
|
59
|
+
loadVideoById(params: {
|
|
60
|
+
videoId: string;
|
|
61
|
+
startSeconds?: number;
|
|
62
|
+
endSeconds?: number;
|
|
63
|
+
}): void;
|
|
64
|
+
|
|
65
|
+
loadVideoByUrl(mediaContentUrl: string, startSeconds?: number): void;
|
|
66
|
+
loadVideoByUrl(params: {
|
|
67
|
+
mediaContentUrl: string;
|
|
68
|
+
startSeconds?: number;
|
|
69
|
+
endSeconds?: number;
|
|
70
|
+
}): void;
|
|
71
|
+
|
|
72
|
+
playVideo(): void;
|
|
73
|
+
pauseVideo(): void;
|
|
74
|
+
stopVideo(): void;
|
|
75
|
+
seekTo(seconds: number, allowSeekAhead: boolean): void;
|
|
76
|
+
|
|
77
|
+
mute(): void;
|
|
78
|
+
unMute(): void;
|
|
79
|
+
isMuted(): void;
|
|
80
|
+
setVolume(volume: number): void;
|
|
81
|
+
getVolume(): void;
|
|
82
|
+
setSize(width: number, height: number): void;
|
|
83
|
+
setLoop(loopPlaylists: boolean): void;
|
|
84
|
+
|
|
85
|
+
getVideoLoadedFraction(): number;
|
|
86
|
+
getPlayerState(): YTPlayerState;
|
|
87
|
+
|
|
88
|
+
getDuration(): number; // in seconds
|
|
89
|
+
getVideoUrl(): string;
|
|
90
|
+
getVideoEmbedCode(): string;
|
|
91
|
+
|
|
92
|
+
getIframe(): HTMLIFrameElement | undefined;
|
|
93
|
+
destroy(): void;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
declare global {
|
|
97
|
+
interface Window {
|
|
98
|
+
YT?: YT;
|
|
99
|
+
onYouTubeIframeAPIReady?: () => void;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { YT } from './YoutubeIframeApi';
|
|
3
|
+
import { YouTubeIframeApiLoader } from './YouTubeIframeApiLoader';
|
|
4
|
+
|
|
5
|
+
export function useYouTubeIframeApi(): YT | undefined {
|
|
6
|
+
const [YT, setYT] = useState<YT | undefined>(undefined);
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
const loader = YouTubeIframeApiLoader.create();
|
|
9
|
+
loader.getApi().then((YT) => setYT(YT));
|
|
10
|
+
}, []);
|
|
11
|
+
return YT;
|
|
12
|
+
}
|