@remotion/promo-pages 5.0.0-canary.3 → 5.0.2-canary
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/.turbo/turbo-make.log +8 -0
- package/bundle.ts +46 -0
- package/dist/Homepage.js +1 -1
- package/dist/homepage/Pricing.js +1 -1
- package/eslint.config.mjs +7 -0
- package/index.html +13 -0
- package/package.json +1 -4
- package/public/fire.mp3 +0 -0
- package/public/img/cluster.png +0 -0
- package/public/img/code-sample-new.png +0 -0
- package/public/img/freelancers/alex.jpeg +0 -0
- package/public/img/freelancers/antoine.jpeg +0 -0
- package/public/img/freelancers/ayush.png +0 -0
- package/public/img/freelancers/benjamin.jpeg +0 -0
- package/public/img/freelancers/default.png +0 -0
- package/public/img/freelancers/florent.jpeg +0 -0
- package/public/img/freelancers/karel.jpeg +0 -0
- package/public/img/freelancers/lorenzo.jpeg +0 -0
- package/public/img/freelancers/mickael.jpeg +0 -0
- package/public/img/freelancers/mohit.jpeg +0 -0
- package/public/img/freelancers/pramod.jpg +0 -0
- package/public/img/freelancers/pranav.jpg +0 -0
- package/public/img/freelancers/rahul.png +0 -0
- package/public/img/freelancers/ray.jpeg +0 -0
- package/public/img/freelancers/stephen.png +0 -0
- package/public/img/freelancers/umungo.png +0 -0
- package/public/img/freelancers/yehor.jpeg +0 -0
- package/public/img/gt-planar-black.woff2 +0 -0
- package/public/img/gt-planar-bold.woff2 +0 -0
- package/public/img/gt-planar-medium.woff2 +0 -0
- package/public/img/gt-planar-regular.woff2 +0 -0
- package/public/img/logo-small.png +0 -0
- package/public/img/mp4.png +0 -0
- package/public/img/player-demo.mp4 +0 -0
- package/public/img/player-example-dark.png +0 -0
- package/public/img/player-example.png +0 -0
- package/public/img/writeinreact.png +0 -0
- package/public/partyhorn.mp3 +0 -0
- package/public/sad.mp3 +0 -0
- package/src/cn.ts +6 -0
- package/src/components/Homepage.tsx +88 -0
- package/src/components/homepage/BackgroundAnimation.tsx +108 -0
- package/src/components/homepage/ChooseTemplate.tsx +57 -0
- package/src/components/homepage/CodeExample.tsx +89 -0
- package/src/components/homepage/CommunityStats.tsx +54 -0
- package/src/components/homepage/CommunityStatsItems.tsx +304 -0
- package/src/components/homepage/Counter.tsx +110 -0
- package/src/components/homepage/Demo/Card.tsx +273 -0
- package/src/components/homepage/Demo/Cards.tsx +129 -0
- package/src/components/homepage/Demo/Comp.tsx +157 -0
- package/src/components/homepage/Demo/CurrentCountry.tsx +97 -0
- package/src/components/homepage/Demo/DemoError.tsx +18 -0
- package/src/components/homepage/Demo/DemoErrorIcon.tsx +32 -0
- package/src/components/homepage/Demo/DemoRender.tsx +166 -0
- package/src/components/homepage/Demo/DigitWheel.tsx +179 -0
- package/src/components/homepage/Demo/DisplayedEmoji.tsx +81 -0
- package/src/components/homepage/Demo/DoneCheckmark.tsx +39 -0
- package/src/components/homepage/Demo/DownloadNudge.tsx +62 -0
- package/src/components/homepage/Demo/DragAndDropNudge.tsx +57 -0
- package/src/components/homepage/Demo/EmojiCard.tsx +198 -0
- package/src/components/homepage/Demo/Minus.tsx +21 -0
- package/src/components/homepage/Demo/PlayPauseButton.tsx +66 -0
- package/src/components/homepage/Demo/PlayerControls.tsx +48 -0
- package/src/components/homepage/Demo/PlayerSeekBar.tsx +325 -0
- package/src/components/homepage/Demo/PlayerVolume.tsx +83 -0
- package/src/components/homepage/Demo/Progress.tsx +38 -0
- package/src/components/homepage/Demo/Spinner.tsx +60 -0
- package/src/components/homepage/Demo/Switcher.tsx +54 -0
- package/src/components/homepage/Demo/Temperature.tsx +44 -0
- package/src/components/homepage/Demo/TemperatureNumber.tsx +68 -0
- package/src/components/homepage/Demo/ThemeNudge.tsx +72 -0
- package/src/components/homepage/Demo/TimeDisplay.tsx +43 -0
- package/src/components/homepage/Demo/TrendingRepos.tsx +106 -0
- package/src/components/homepage/Demo/icons.tsx +114 -0
- package/src/components/homepage/Demo/index.tsx +158 -0
- package/src/components/homepage/Demo/math.ts +43 -0
- package/src/components/homepage/Demo/types.ts +6 -0
- package/src/components/homepage/Editor.tsx +67 -0
- package/src/components/homepage/EvaluateRemotion.tsx +92 -0
- package/src/components/homepage/FreePricing.tsx +295 -0
- package/src/components/homepage/GetStartedStrip.tsx +77 -0
- package/src/components/homepage/GitHubButton.tsx +23 -0
- package/src/components/homepage/IconForTemplate.tsx +154 -0
- package/src/components/homepage/IfYouKnowReact.tsx +29 -0
- package/src/components/homepage/InfoTooltip.tsx +25 -0
- package/src/components/homepage/MoreTemplatesButton.tsx +29 -0
- package/src/components/homepage/MuxVideo.tsx +68 -0
- package/src/components/homepage/NewsletterButton.tsx +88 -0
- package/src/components/homepage/Pricing.tsx +49 -0
- package/src/components/homepage/PricingBulletPoint.tsx +50 -0
- package/src/components/homepage/RealMp4Videos.tsx +50 -0
- package/src/components/homepage/Spacer.tsx +5 -0
- package/src/components/homepage/TemplateIcon.tsx +36 -0
- package/src/components/homepage/TextInput.tsx +62 -0
- package/src/components/homepage/TrustedByBanner.tsx +194 -0
- package/src/components/homepage/VideoApps.tsx +231 -0
- package/src/components/homepage/VideoAppsShowcase.tsx +276 -0
- package/src/components/homepage/VideoAppsTitle.tsx +24 -0
- package/src/components/homepage/VideoPlayerWithControls.tsx +188 -0
- package/src/components/homepage/WriteInReact.tsx +34 -0
- package/src/components/homepage/YouAreHere.tsx +30 -0
- package/src/components/homepage/custom.css +57 -0
- package/src/components/homepage/layout/Button.tsx +93 -0
- package/src/components/homepage/layout/colors.ts +17 -0
- package/src/components/homepage/layout/use-color-mode.tsx +44 -0
- package/src/components/homepage/layout/use-el-size.ts +51 -0
- package/src/components/homepage/layout/use-mobile-layout.ts +8 -0
- package/src/components/homepage/video-player.css +24 -0
- package/src/components/icons/blank.tsx +13 -0
- package/src/components/icons/clone.tsx +10 -0
- package/src/components/icons/code-hike.tsx +15 -0
- package/src/components/icons/cubes.tsx +13 -0
- package/src/components/icons/js.tsx +17 -0
- package/src/components/icons/next.tsx +64 -0
- package/src/components/icons/overlay.tsx +24 -0
- package/src/components/icons/remix.tsx +24 -0
- package/src/components/icons/skia.tsx +13 -0
- package/src/components/icons/stargazer.tsx +13 -0
- package/src/components/icons/still.tsx +13 -0
- package/src/components/icons/tailwind.tsx +22 -0
- package/src/components/icons/tiktok.tsx +13 -0
- package/src/components/icons/ts.tsx +18 -0
- package/src/components/icons/tts.tsx +13 -0
- package/src/components/icons/undo.tsx +11 -0
- package/src/components/icons/waveform.tsx +13 -0
- package/src/fonts.css +30 -0
- package/src/index.css +74 -0
- package/src/main.tsx +12 -0
- package/tsconfig.json +15 -0
- package/vite.config.ts +9 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import React, {useEffect, useRef, useState} from 'react';
|
|
2
|
+
|
|
3
|
+
import {BlueButton} from './layout/Button';
|
|
4
|
+
import {MuxVideo} from './MuxVideo';
|
|
5
|
+
import {SectionTitle} from './VideoAppsTitle';
|
|
6
|
+
|
|
7
|
+
const tabs = [
|
|
8
|
+
'Music visualization',
|
|
9
|
+
'Captions',
|
|
10
|
+
'Screencast',
|
|
11
|
+
'Year in review',
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
const videoApps = [
|
|
15
|
+
{
|
|
16
|
+
title: 'Banger.Show',
|
|
17
|
+
description:
|
|
18
|
+
'The all-in-one 3D visual creation tool for ambitious artists. Seamlessly craft visuals that match your sound and propel your brand forward.',
|
|
19
|
+
link: 'https://banger.show?ref=remotion',
|
|
20
|
+
videoWidth: 1080,
|
|
21
|
+
videoHeight: 1080,
|
|
22
|
+
muxId: 'riYdneJ2zu1Vqiayoe1qAZXcSIRq0201tHgSBbh9JbtlU',
|
|
23
|
+
buttonText: 'Banger.Show website',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
title: 'Submagic',
|
|
27
|
+
description:
|
|
28
|
+
'A video editor for creating short-form content fast. Designed for creators, teams and agencies, it accelerates video editing with AI-powered features such as descriptions, zooms, sound effects and music.',
|
|
29
|
+
additionalInfo: '',
|
|
30
|
+
link: 'https://www.submagic.co/?ref=remotion',
|
|
31
|
+
videoWidth: 540,
|
|
32
|
+
videoHeight: 1080,
|
|
33
|
+
muxId: 'pxqGEjlBBntnXrEe4v00pYUBw3FPgUPKumfhSym00Vs004',
|
|
34
|
+
buttonText: 'Submagic website',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
title: 'Remotion Recorder',
|
|
38
|
+
description:
|
|
39
|
+
'The Remotion Recorder is a video production tool built entirely in JavaScript. Create high-quality videos that feel native on each platform while only editing them once.',
|
|
40
|
+
|
|
41
|
+
link: 'https://www.remotion.pro/recorder',
|
|
42
|
+
videoWidth: 1080,
|
|
43
|
+
videoHeight: 1080,
|
|
44
|
+
muxId: 'pHlwqDZFUH00Aubo9M001ty3gZ6YW8z689XTd9R479ayE',
|
|
45
|
+
buttonText: 'More infos',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
title: 'GitHub Unwrapped',
|
|
49
|
+
description:
|
|
50
|
+
'Your coding year in review. Get a personalized video of your GitHub activity.',
|
|
51
|
+
additionalInfo:
|
|
52
|
+
'Uncover your go-to language, peak productivity hours, and track your GitHub impact – all in one video.',
|
|
53
|
+
link: 'https://githubunwrapped.com/',
|
|
54
|
+
videoWidth: 1080,
|
|
55
|
+
videoHeight: 1080,
|
|
56
|
+
muxId: 'OwQFvqomOR00q6yj5SWwaA7DBg01NaCPKcOvczoZqCty00',
|
|
57
|
+
buttonText: 'GitHub Unwrapped website',
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
const VideoAppsShowcase: React.FC = () => {
|
|
62
|
+
const [activeTab, setActiveTab] = useState(0);
|
|
63
|
+
const [isMuted, setIsMuted] = useState(true);
|
|
64
|
+
const videoRef = useRef<HTMLVideoElement>(null);
|
|
65
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
66
|
+
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
const observer = new IntersectionObserver(
|
|
69
|
+
(entries) => {
|
|
70
|
+
if (entries[0].isIntersecting) {
|
|
71
|
+
if (videoRef.current && videoRef.current.paused) {
|
|
72
|
+
videoRef.current.muted = true; // Ensure video is muted before autoplay
|
|
73
|
+
setIsMuted(true); // Update state to reflect muted status
|
|
74
|
+
videoRef.current
|
|
75
|
+
.play()
|
|
76
|
+
.then(() => {})
|
|
77
|
+
.catch((error) => {
|
|
78
|
+
// eslint-disable-next-line no-console
|
|
79
|
+
console.error('Playback error:', error);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
} else if (videoRef.current && !videoRef.current.paused) {
|
|
83
|
+
videoRef.current.pause();
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
{threshold: 0.5},
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const currentContainer = containerRef.current;
|
|
90
|
+
if (currentContainer) {
|
|
91
|
+
observer.observe(currentContainer);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return () => {
|
|
95
|
+
if (currentContainer) {
|
|
96
|
+
observer.unobserve(currentContainer);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
}, []);
|
|
100
|
+
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
const video = videoRef.current;
|
|
103
|
+
if (video) {
|
|
104
|
+
video.pause();
|
|
105
|
+
video.currentTime = 0;
|
|
106
|
+
video.load();
|
|
107
|
+
|
|
108
|
+
// Check if the video is visible and play it if it is
|
|
109
|
+
const observer = new IntersectionObserver(
|
|
110
|
+
(entries) => {
|
|
111
|
+
if (entries[0].isIntersecting) {
|
|
112
|
+
// Introduce a delay before playing the video
|
|
113
|
+
if (video) {
|
|
114
|
+
video.muted = true; // Ensure video is muted before autoplay
|
|
115
|
+
setIsMuted(true); // Update state to reflect muted status
|
|
116
|
+
video
|
|
117
|
+
.play()
|
|
118
|
+
.then(() => {})
|
|
119
|
+
.catch((error) => {
|
|
120
|
+
// eslint-disable-next-line no-console
|
|
121
|
+
console.error('Playback error:', error);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
{threshold: 0.5},
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
observer.observe(video);
|
|
130
|
+
|
|
131
|
+
return () => {
|
|
132
|
+
observer.disconnect();
|
|
133
|
+
video.muted = false; // Unmute the video when it's no longer visible
|
|
134
|
+
if (video) {
|
|
135
|
+
video.pause();
|
|
136
|
+
video.currentTime = 0;
|
|
137
|
+
video.load();
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}, [activeTab]);
|
|
142
|
+
|
|
143
|
+
const handlePlayPause = () => {
|
|
144
|
+
if (videoRef.current) {
|
|
145
|
+
if (videoRef.current.paused) {
|
|
146
|
+
const playPromise = videoRef.current.play();
|
|
147
|
+
|
|
148
|
+
if (playPromise !== undefined) {
|
|
149
|
+
playPromise
|
|
150
|
+
.then(() => {
|
|
151
|
+
// Playback started successfully
|
|
152
|
+
})
|
|
153
|
+
.catch((error) => {
|
|
154
|
+
// Auto-play was prevented or there was an error
|
|
155
|
+
// eslint-disable-next-line no-console
|
|
156
|
+
console.error('Playback error:', error);
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
} else {
|
|
160
|
+
videoRef.current.pause();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const handleMuteToggle = () => {
|
|
166
|
+
if (videoRef.current) {
|
|
167
|
+
const newMutedState = !videoRef.current.muted;
|
|
168
|
+
videoRef.current.muted = newMutedState;
|
|
169
|
+
setIsMuted(newMutedState);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
return (
|
|
174
|
+
<div ref={containerRef}>
|
|
175
|
+
<SectionTitle>Use Cases</SectionTitle>
|
|
176
|
+
<div className={'flex justify-center mb-4'}>
|
|
177
|
+
{tabs.map((tab, index) => (
|
|
178
|
+
<button
|
|
179
|
+
key={tab}
|
|
180
|
+
type="button"
|
|
181
|
+
data-active={index === activeTab}
|
|
182
|
+
className={`bg-transparent border-none mx-3 my-4 cursor-pointer text-base fontbrand font-bold transition-colors text-muted data-[active=true]:text-brand`}
|
|
183
|
+
onClick={() => setActiveTab(index)}
|
|
184
|
+
>
|
|
185
|
+
{tab}
|
|
186
|
+
</button>
|
|
187
|
+
))}
|
|
188
|
+
</div>
|
|
189
|
+
<div className={'card flex p-0 overflow-hidden'}>
|
|
190
|
+
<div className={'flex-1 flex flex-col lg:flex-row justify-center'}>
|
|
191
|
+
<div
|
|
192
|
+
className={
|
|
193
|
+
'w-full max-w-[500px] aspect-square relative overflow-hidden bg-[#eee]'
|
|
194
|
+
}
|
|
195
|
+
onClick={handlePlayPause}
|
|
196
|
+
>
|
|
197
|
+
<MuxVideo
|
|
198
|
+
ref={videoRef}
|
|
199
|
+
muxId={videoApps[activeTab].muxId}
|
|
200
|
+
className={
|
|
201
|
+
'absolute left-0 top-0 w-full h-full object-contain rounded-lg rounded-tr-none rounded-br-none'
|
|
202
|
+
}
|
|
203
|
+
loop
|
|
204
|
+
playsInline
|
|
205
|
+
muted={isMuted}
|
|
206
|
+
/>
|
|
207
|
+
|
|
208
|
+
{isMuted && (
|
|
209
|
+
<button
|
|
210
|
+
type="button"
|
|
211
|
+
className={
|
|
212
|
+
'absolute bottom-2.5 right-2.5 bg-white text-black rounded-full w-8 h-8 flex justify-center items-center text-base cursor-pointer transition-colors border-2 border-black border-solid'
|
|
213
|
+
}
|
|
214
|
+
onClick={(e) => {
|
|
215
|
+
e.stopPropagation();
|
|
216
|
+
handleMuteToggle();
|
|
217
|
+
}}
|
|
218
|
+
>
|
|
219
|
+
<svg style={{width: 24}} viewBox="0 0 576 512">
|
|
220
|
+
<path
|
|
221
|
+
fill="black"
|
|
222
|
+
d="M0 160L0 352l128 0L272 480l48 0 0-448-48 0L128 160 0 160zm441 23l-17-17L390.1 200l17 17 39 39-39 39-17 17L424 345.9l17-17 39-39 39 39 17 17L569.9 312l-17-17-39-39 39-39 17-17L536 166.1l-17 17-39 39-39-39z"
|
|
223
|
+
/>
|
|
224
|
+
</svg>
|
|
225
|
+
</button>
|
|
226
|
+
)}
|
|
227
|
+
</div>
|
|
228
|
+
<div className={'pl-4 flex-1 lg:pl-4 flex flex-col h-full pt-8 pb-6'}>
|
|
229
|
+
<div className="text-4xl font-bold fontbrand">
|
|
230
|
+
{videoApps[activeTab].title}
|
|
231
|
+
</div>
|
|
232
|
+
<div className="text-muted mt-4 text-base fontbrand">
|
|
233
|
+
{videoApps[activeTab].description}
|
|
234
|
+
</div>
|
|
235
|
+
{videoApps[activeTab].additionalInfo ? (
|
|
236
|
+
<div className="text-muted mt-4 text-base fontbrand">
|
|
237
|
+
{videoApps[activeTab].additionalInfo}
|
|
238
|
+
</div>
|
|
239
|
+
) : null}
|
|
240
|
+
<div className="h-5" />
|
|
241
|
+
<a
|
|
242
|
+
target="_blank"
|
|
243
|
+
href={videoApps[activeTab].link}
|
|
244
|
+
style={{textDecoration: 'none'}}
|
|
245
|
+
>
|
|
246
|
+
<BlueButton loading={false} size="sm">
|
|
247
|
+
{videoApps[activeTab].buttonText}
|
|
248
|
+
</BlueButton>
|
|
249
|
+
</a>
|
|
250
|
+
</div>
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
<div
|
|
254
|
+
style={{
|
|
255
|
+
marginTop: '1rem',
|
|
256
|
+
justifyContent: 'center',
|
|
257
|
+
display: 'flex',
|
|
258
|
+
}}
|
|
259
|
+
>
|
|
260
|
+
<div
|
|
261
|
+
style={{
|
|
262
|
+
fontFamily: 'GTPlanar',
|
|
263
|
+
}}
|
|
264
|
+
>
|
|
265
|
+
For more examples see our{' '}
|
|
266
|
+
<a href="/showcase" className="bluelink">
|
|
267
|
+
Showcase page
|
|
268
|
+
</a>
|
|
269
|
+
.
|
|
270
|
+
</div>
|
|
271
|
+
</div>
|
|
272
|
+
</div>
|
|
273
|
+
);
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
export default VideoAppsShowcase;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
export const VideoAppsTitle: React.FC = () => {
|
|
4
|
+
return (
|
|
5
|
+
<div className={'text-center'}>
|
|
6
|
+
<h2 className={'fontbrand text-4xl'}>
|
|
7
|
+
Build video <span className={'fontbrand'}>apps</span>
|
|
8
|
+
</h2>
|
|
9
|
+
<p>
|
|
10
|
+
Use our suite of tools to build apps that lets others create videos.
|
|
11
|
+
</p>
|
|
12
|
+
</div>
|
|
13
|
+
);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const SectionTitle: React.FC<{readonly children: React.ReactNode}> = ({
|
|
17
|
+
children,
|
|
18
|
+
}) => {
|
|
19
|
+
return (
|
|
20
|
+
<div className={'text-center'}>
|
|
21
|
+
<h2 className={'fontbrand text-4xl'}>{children}</h2>
|
|
22
|
+
</div>
|
|
23
|
+
);
|
|
24
|
+
};
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
/* eslint-disable react/require-default-props */
|
|
3
|
+
import Hls from 'hls.js';
|
|
4
|
+
import type Plyr from 'plyr';
|
|
5
|
+
import 'plyr/dist/plyr.css';
|
|
6
|
+
import type {MutableRefObject} from 'react';
|
|
7
|
+
import {forwardRef, useCallback, useEffect, useRef, useState} from 'react';
|
|
8
|
+
import './video-player.css';
|
|
9
|
+
|
|
10
|
+
export interface HTMLVideoElementWithPlyr extends HTMLVideoElement {
|
|
11
|
+
plyr: Plyr;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const useCombinedRefs = function (
|
|
15
|
+
...refs: (
|
|
16
|
+
| ((instance: HTMLVideoElementWithPlyr | null) => void)
|
|
17
|
+
| MutableRefObject<HTMLVideoElementWithPlyr | null>
|
|
18
|
+
| null
|
|
19
|
+
)[]
|
|
20
|
+
): MutableRefObject<HTMLVideoElementWithPlyr | null> {
|
|
21
|
+
const targetRef = useRef(null);
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
refs.forEach((ref) => {
|
|
25
|
+
if (!ref) return;
|
|
26
|
+
|
|
27
|
+
if (typeof ref === 'function') {
|
|
28
|
+
ref(targetRef.current);
|
|
29
|
+
} else {
|
|
30
|
+
ref.current = targetRef.current;
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}, [refs]);
|
|
34
|
+
|
|
35
|
+
return targetRef;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/*
|
|
39
|
+
* We need to set the width/height of the player depending on what the dimensions of
|
|
40
|
+
* the underlying video source is.
|
|
41
|
+
*
|
|
42
|
+
* On most platforms we know the dimensions on 'loadedmetadata'
|
|
43
|
+
* On Desktop Safari we don't know the dimensions until 'canplay'
|
|
44
|
+
*
|
|
45
|
+
* At first, I tried to get the dimensions of the video from these callbacks, that worked
|
|
46
|
+
* great except for on moble Safari. On Mobile Safari none of those callbacks fire until
|
|
47
|
+
* there is some user interaction :(
|
|
48
|
+
*
|
|
49
|
+
* BUT! There is a brilliant hack here. We can create a `display: none` `img` element in the
|
|
50
|
+
* DOM, load up the poster image.
|
|
51
|
+
*
|
|
52
|
+
* Since the poster image will have the same dimensions of the video, now we know if the video
|
|
53
|
+
* is vertical and now we can style the proper width/height so the layout doesn't have a sudden
|
|
54
|
+
* jump or resize.
|
|
55
|
+
*
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
type Props = {
|
|
59
|
+
readonly playbackId: string;
|
|
60
|
+
readonly poster: string;
|
|
61
|
+
readonly currentTime?: number;
|
|
62
|
+
readonly onLoaded: () => void;
|
|
63
|
+
readonly onError: (error: ErrorEvent) => void;
|
|
64
|
+
readonly onSize: (dim: {width: number; height: number}) => void;
|
|
65
|
+
readonly autoPlay?: boolean;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
type SizedEvent = {
|
|
69
|
+
target: {
|
|
70
|
+
width: number;
|
|
71
|
+
height: number;
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const VideoPlayerWithControls = forwardRef<
|
|
76
|
+
HTMLVideoElementWithPlyr,
|
|
77
|
+
Props
|
|
78
|
+
>(
|
|
79
|
+
(
|
|
80
|
+
{playbackId, poster, currentTime, onLoaded, onError, onSize, autoPlay},
|
|
81
|
+
ref,
|
|
82
|
+
) => {
|
|
83
|
+
const videoRef = useRef<HTMLVideoElementWithPlyr>(null);
|
|
84
|
+
const metaRef = useCombinedRefs(ref, videoRef);
|
|
85
|
+
const playerRef = useRef<Plyr | null>(null);
|
|
86
|
+
const [playerInitTime] = useState(Date.now());
|
|
87
|
+
|
|
88
|
+
const videoError = useCallback(
|
|
89
|
+
(event: ErrorEvent) => onError(event),
|
|
90
|
+
[onError],
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const onImageLoad = useCallback(
|
|
94
|
+
(event: SizedEvent) => {
|
|
95
|
+
const [w, h] = [event.target.width, event.target.height];
|
|
96
|
+
if (w && h) {
|
|
97
|
+
onSize({width: w, height: h});
|
|
98
|
+
onLoaded();
|
|
99
|
+
} else {
|
|
100
|
+
onLoaded();
|
|
101
|
+
|
|
102
|
+
console.error('Error getting img dimensions', event);
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
[onLoaded, onSize],
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
/*
|
|
109
|
+
* See comment above -- we're loading the poster image just so we can grab the dimensions
|
|
110
|
+
* which determines styles for the player
|
|
111
|
+
*/
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
const img = new Image();
|
|
114
|
+
img.onload = (evt) => onImageLoad(evt as unknown as SizedEvent);
|
|
115
|
+
img.src = poster;
|
|
116
|
+
}, [onImageLoad, poster]);
|
|
117
|
+
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
const video = videoRef.current;
|
|
120
|
+
const src = `https://stream.mux.com/${playbackId}.m3u8`;
|
|
121
|
+
let hls: Hls | null;
|
|
122
|
+
hls = null;
|
|
123
|
+
if (video) {
|
|
124
|
+
video.addEventListener('error', videoError);
|
|
125
|
+
const Plyr = require('plyr');
|
|
126
|
+
playerRef.current = new Plyr(video, {
|
|
127
|
+
previewThumbnails: {
|
|
128
|
+
enabled: true,
|
|
129
|
+
src: `https://image.mux.com/${playbackId}/storyboard.vtt`,
|
|
130
|
+
},
|
|
131
|
+
storage: {enabled: false},
|
|
132
|
+
fullscreen: {
|
|
133
|
+
iosNative: true,
|
|
134
|
+
},
|
|
135
|
+
captions: {active: true, language: 'auto', update: true},
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
if (video.canPlayType('application/vnd.apple.mpegurl')) {
|
|
139
|
+
// This will run in safari, where HLS is supported natively
|
|
140
|
+
video.src = src;
|
|
141
|
+
} else if (Hls.isSupported()) {
|
|
142
|
+
// This will run in all other modern browsers
|
|
143
|
+
hls = new Hls();
|
|
144
|
+
hls.loadSource(src);
|
|
145
|
+
hls.attachMedia(video);
|
|
146
|
+
hls.on(Hls.Events.ERROR, (_event, data) => {
|
|
147
|
+
if (data.fatal) {
|
|
148
|
+
videoError(new ErrorEvent('HLS.js fatal error'));
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
} else {
|
|
152
|
+
console.error(
|
|
153
|
+
'This is an old browser that does not support MSE https://developer.mozilla.org/en-US/docs/Web/API/Media_Source_Extensions_API',
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return () => {
|
|
159
|
+
if (video) {
|
|
160
|
+
video.removeEventListener('error', videoError);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (hls) {
|
|
164
|
+
hls.destroy();
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
}, [playbackId, playerInitTime, videoError, videoRef]);
|
|
168
|
+
|
|
169
|
+
useEffect(() => {
|
|
170
|
+
const video = videoRef.current;
|
|
171
|
+
if (currentTime && video) {
|
|
172
|
+
video.currentTime = currentTime;
|
|
173
|
+
}
|
|
174
|
+
}, [currentTime]);
|
|
175
|
+
|
|
176
|
+
return (
|
|
177
|
+
<div className="video-container">
|
|
178
|
+
<video
|
|
179
|
+
ref={metaRef}
|
|
180
|
+
autoPlay={autoPlay}
|
|
181
|
+
poster={poster}
|
|
182
|
+
controls
|
|
183
|
+
playsInline
|
|
184
|
+
/>
|
|
185
|
+
</div>
|
|
186
|
+
);
|
|
187
|
+
},
|
|
188
|
+
);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {ChooseTemplate} from './ChooseTemplate';
|
|
3
|
+
import {GetStarted} from './GetStartedStrip';
|
|
4
|
+
|
|
5
|
+
export const WriteInReact: React.FC = () => {
|
|
6
|
+
return (
|
|
7
|
+
<div>
|
|
8
|
+
<h1
|
|
9
|
+
className="text-5xl lg:text-[5em] text-center fontbrand font-black leading-none "
|
|
10
|
+
style={{
|
|
11
|
+
textShadow: '0 5px 30px var(--background)',
|
|
12
|
+
}}
|
|
13
|
+
>
|
|
14
|
+
Make videos programmatically.
|
|
15
|
+
</h1>
|
|
16
|
+
<p
|
|
17
|
+
style={{
|
|
18
|
+
textShadow: '0 5px 30px var(--background)',
|
|
19
|
+
}}
|
|
20
|
+
className="font-medium text-center text-lg"
|
|
21
|
+
>
|
|
22
|
+
Create real MP4 videos with React. <br />
|
|
23
|
+
Parametrize content, render server-side and build applications.
|
|
24
|
+
</p>
|
|
25
|
+
<br />
|
|
26
|
+
<div>
|
|
27
|
+
<GetStarted />
|
|
28
|
+
</div>
|
|
29
|
+
<br />
|
|
30
|
+
<br />
|
|
31
|
+
<ChooseTemplate />
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
const container: React.CSSProperties = {
|
|
4
|
+
width: '100%',
|
|
5
|
+
display: 'inline-flex',
|
|
6
|
+
justifyContent: 'center',
|
|
7
|
+
position: 'relative',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const label: React.CSSProperties = {
|
|
11
|
+
backgroundColor: 'var(--ifm-color-primary)',
|
|
12
|
+
fontWeight: 'bold',
|
|
13
|
+
color: 'white',
|
|
14
|
+
paddingLeft: 8,
|
|
15
|
+
paddingRight: 8,
|
|
16
|
+
paddingTop: 4,
|
|
17
|
+
paddingBottom: 4,
|
|
18
|
+
borderRadius: 6,
|
|
19
|
+
fontSize: 13,
|
|
20
|
+
position: 'absolute',
|
|
21
|
+
marginTop: -25,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const YouAreHere: React.FC = () => {
|
|
25
|
+
return (
|
|
26
|
+
<div style={container}>
|
|
27
|
+
<div style={label}>YOU ARE HERE</div>
|
|
28
|
+
</div>
|
|
29
|
+
);
|
|
30
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--ifm-color-primary: #0b84f3;
|
|
3
|
+
--ifm-color-primary-dark: #0a77db;
|
|
4
|
+
--ifm-color-primary-darker: #0970cf;
|
|
5
|
+
--ifm-color-primary-darkest: #085caa;
|
|
6
|
+
--ifm-color-primary-light: #2290f5;
|
|
7
|
+
--ifm-color-primary-lighter: #2f96f6;
|
|
8
|
+
--ifm-color-primary-lightest: #53a9f7;
|
|
9
|
+
--ifm-code-font-size: 100%;
|
|
10
|
+
--ifm-font-family-base: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu,
|
|
11
|
+
Cantarell, Noto Sans, sans-serif, BlinkMacSystemFont, 'Segoe UI', Helvetica,
|
|
12
|
+
Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
|
|
13
|
+
--ifm-font-size-base: 16px;
|
|
14
|
+
--ifm-font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas,
|
|
15
|
+
'Liberation Mono', 'Courier New', monospace;
|
|
16
|
+
--ifm-pre-line-height:
|
|
17
|
+
;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
[data-theme='light'] {
|
|
21
|
+
--background: #f8fafc;
|
|
22
|
+
--footer-background: #fcfcfc;
|
|
23
|
+
--footer-border: #eaeaea;
|
|
24
|
+
--text-color: #000;
|
|
25
|
+
--blue-button-color: #084696;
|
|
26
|
+
--blue-underlay: var(--ifm-color-primary);
|
|
27
|
+
--plain-button: #fff;
|
|
28
|
+
--blue-underlay-hover: #d5e5fd;
|
|
29
|
+
--light-text-color: #777;
|
|
30
|
+
--subtitle: #666;
|
|
31
|
+
--clear-hover: rgba(0, 0, 0, 0.04);
|
|
32
|
+
--border-color: rgb(234, 234, 234);
|
|
33
|
+
--box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
|
|
34
|
+
--ifm-out-of-focus: #eaeaea;
|
|
35
|
+
--box-stroke: #000;
|
|
36
|
+
--ifm-background-surface-color: #fff;
|
|
37
|
+
--ifm-color-emphasis-200: #ebedf0;
|
|
38
|
+
}
|
|
39
|
+
[data-theme='dark'] {
|
|
40
|
+
--background: #18191a;
|
|
41
|
+
--footer-background: #1f1f1f;
|
|
42
|
+
--footer-border: #2f2f2f;
|
|
43
|
+
--text-color: #fff;
|
|
44
|
+
--plain-button: var(--blue-underlay);
|
|
45
|
+
--blue-underlay: #424243;
|
|
46
|
+
--blue-underlay-hover: #5b5c5e;
|
|
47
|
+
--clear-hover: rgba(255, 255, 255, 0.06);
|
|
48
|
+
--blue-button-color: white;
|
|
49
|
+
--light-text-color: #aaa;
|
|
50
|
+
--subtitle: #8d8d8d;
|
|
51
|
+
--border-color: rgb(42, 42, 42);
|
|
52
|
+
--box-shadow: 0 1px 8px rgba(255, 255, 255, 0.2);
|
|
53
|
+
--ifm-out-of-focus: #505050;
|
|
54
|
+
--box-stroke: gray;
|
|
55
|
+
--ifm-background-surface-color: #242526;
|
|
56
|
+
--ifm-color-emphasis-200: #444950;
|
|
57
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import {opacify} from 'polished';
|
|
2
|
+
import type {ButtonHTMLAttributes, DetailedHTMLProps} from 'react';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import {cn} from '../../../cn';
|
|
5
|
+
import {RED, UNDERLAY_RED} from './colors';
|
|
6
|
+
|
|
7
|
+
type ExtraProps = {
|
|
8
|
+
readonly size: Size;
|
|
9
|
+
readonly background: string;
|
|
10
|
+
readonly hoverColor?: string;
|
|
11
|
+
readonly color: string;
|
|
12
|
+
readonly loading: boolean;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type Size = 'sm' | 'bg';
|
|
16
|
+
|
|
17
|
+
type Props = DetailedHTMLProps<
|
|
18
|
+
ButtonHTMLAttributes<HTMLButtonElement>,
|
|
19
|
+
HTMLButtonElement
|
|
20
|
+
> &
|
|
21
|
+
ExtraProps;
|
|
22
|
+
type MandatoryProps = Omit<ExtraProps, 'background' | 'color' | 'hoverColor'>;
|
|
23
|
+
type PrestyledProps = DetailedHTMLProps<
|
|
24
|
+
ButtonHTMLAttributes<HTMLButtonElement>,
|
|
25
|
+
HTMLButtonElement
|
|
26
|
+
> &
|
|
27
|
+
MandatoryProps;
|
|
28
|
+
|
|
29
|
+
export const Button: React.FC<Props> = (props) => {
|
|
30
|
+
const {children, loading, hoverColor, color, size, className, ...other} =
|
|
31
|
+
props;
|
|
32
|
+
const actualDisabled = other.disabled || loading;
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<button
|
|
36
|
+
type="button"
|
|
37
|
+
className={cn(
|
|
38
|
+
'text-base rounded-lg font-bold appearance-none border-2 border-solid border-black border-b-4 font-sans flex flex-row items-center justify-center ',
|
|
39
|
+
className,
|
|
40
|
+
)}
|
|
41
|
+
disabled={actualDisabled}
|
|
42
|
+
{...other}
|
|
43
|
+
style={{
|
|
44
|
+
...(props.style ?? {}),
|
|
45
|
+
padding:
|
|
46
|
+
props.style?.padding ??
|
|
47
|
+
(props.size === 'sm' ? '10px 16px' : '16px 22px'),
|
|
48
|
+
color: props.color,
|
|
49
|
+
cursor: props.disabled ? 'default' : 'pointer',
|
|
50
|
+
backgroundColor: props.background,
|
|
51
|
+
opacity: props.disabled ? 0.7 : 1,
|
|
52
|
+
}}
|
|
53
|
+
>
|
|
54
|
+
{children}
|
|
55
|
+
</button>
|
|
56
|
+
);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const BlueButton: React.FC<PrestyledProps> = (props) => {
|
|
60
|
+
return <Button {...props} background="var(--blue-underlay)" color="white" />;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const PlainButton: React.FC<PrestyledProps> = (props) => {
|
|
64
|
+
return (
|
|
65
|
+
<Button
|
|
66
|
+
{...props}
|
|
67
|
+
background="var(--plain-button)"
|
|
68
|
+
color="var(--text-color)"
|
|
69
|
+
/>
|
|
70
|
+
);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const RedButton: React.FC<PrestyledProps> = (props) => {
|
|
74
|
+
return (
|
|
75
|
+
<Button
|
|
76
|
+
{...props}
|
|
77
|
+
background={UNDERLAY_RED}
|
|
78
|
+
hoverColor={opacify(0.1, UNDERLAY_RED)}
|
|
79
|
+
color={RED}
|
|
80
|
+
/>
|
|
81
|
+
);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export const ClearButton: React.FC<PrestyledProps> = (props) => {
|
|
85
|
+
return (
|
|
86
|
+
<Button
|
|
87
|
+
{...props}
|
|
88
|
+
background="transparent"
|
|
89
|
+
color="var(--text-color)"
|
|
90
|
+
hoverColor="var(--clear-hover)"
|
|
91
|
+
/>
|
|
92
|
+
);
|
|
93
|
+
};
|