You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
docs(rules): video player imperative-only, install latest, shorten section
- Video player: single pattern only (imperative element, no React-managed ref)
- One example: createElement, append to container ref, pass to videoPlayer; cleanup dispose + removeChild guard
- Consolidate common errors (Invalid target, removeChild, CSP) into shorter entries
- Install Cloudinary packages: explicit 'install latest' rule, no version pin
- Project setup and checklist updated to imperative + fallback to AdvancedVideo
- **Signed uploads**: Do not use only `uploadPreset`; use the pattern under "Secure (Signed) Uploads" (uploadSignature as function, fetch api_key, server includes upload_preset in signature).
56
56
57
57
**4. Video player**
58
-
- Use `cloudName` from `import.meta.env.VITE_CLOUDINARY_CLOUD_NAME` in player config. Validate it before init (e.g. if !cloudName, set error state). See "Cloudinary Video Player (The Player)" for full pattern (named import `videoPlayer`, `player.source({ publicId })`, refs, cleanup).
58
+
- Use imperative video element only (create with document.createElement, append to container ref, pass to videoPlayer). See "Cloudinary Video Player (The Player)" for the full pattern.
59
59
60
60
**5. Summary for rules-only users**
61
61
- **Env**: Use your bundler's client env prefix and access (Vite: `VITE_` + `import.meta.env.VITE_*`; see "Other bundlers" if not Vite).
62
62
- **Reusable instance**: One config file that creates and exports `cld` (and optionally `uploadPreset`) from `@cloudinary/url-gen`; use it everywhere.
63
63
- **Upload widget**: Script in index.html (or equivalent); create widget once in useEffect with ref; unsigned = cloudName + uploadPreset; signed = use uploadSignature function and backend.
64
-
- **Video player**: Named import `videoPlayer`, source object, refs, dispose in cleanup.
64
+
- **Video player**: Imperative video element (createElement, append to container ref, pass to videoPlayer); dispose + removeChild in cleanup; fall back to AdvancedVideo if init fails.
65
65
66
66
**If the user is not using Vite:** Use their bundler's client env prefix and access in the config file and everywhere you read env. Examples: Create React App → `REACT_APP_CLOUDINARY_CLOUD_NAME`, `process.env.REACT_APP_CLOUDINARY_CLOUD_NAME`; Next.js (client) → `NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME`, `process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME`. The rest (cld instance, widget options, video player) is the same.
3. Is the preset set to **Unsigned** (not Signed)?
90
90
4. Was the dev server restarted after adding/updating `.env`?
91
91
92
+
## Installing Cloudinary packages
93
+
- ✅ **Install the latest**: When adding Cloudinary packages, use `npm install <package>` **with no version** so npm installs the latest compatible version (e.g. `npm install cloudinary-video-player`). In package.json use a **caret range** (e.g. `"cloudinary-video-player": "^1.0.0"`) so future installs get the latest compatible. Do not pin to an exact version unless you have verified it exists on npm.
94
+
- ✅ **Package names only**: Use **only** these names: `@cloudinary/react`, `@cloudinary/url-gen`, `cloudinary-video-player` (standalone player), `cloudinary` (Node server-side only). Do not invent names (e.g. no `@cloudinary/video-player`).
95
+
- ❌ **WRONG**: `npm install cloudinary-video-player@1.2.3` or `"cloudinary-video-player": "1.2.3"` (exact pin) — versions may not exist and break installs.
96
+
- ✅ **Correct**: `npm install cloudinary-video-player` (no version) or in package.json: `"cloudinary-video-player": "^1.0.0"` (caret = latest compatible).
- Use when: user wants to show/display a video. Works with `cld.video()` like images with `cld.image()`
370
376
371
377
**2. Cloudinary Video Player** (`cloudinary-video-player`) — The **player**
372
-
- Full-featured video player (styled UI, controls, playlists, recommendations, ads, chapters)
373
-
- Use when: user asks for a "video player" or needs player features (playlists, ads, etc.)
374
-
- Separate package; requires CSS and useEffect with `player.dispose()` cleanup
378
+
- Full-featured video player (styled UI, controls, playlists). Use when the user asks for a "video player."
379
+
- **Use imperative video element only** (create with document.createElement, append to container ref); do not pass a React-managed `<video ref>`. See "Cloudinary Video Player (The Player)" below.
375
380
376
381
### AdvancedVideo (React SDK - For Displaying a Video)
377
382
- ✅ **Purpose**: Display a video with Cloudinary transformations (resize, effects, etc.). It is **not** a full player — it is for showing a video. For a player, use Cloudinary Video Player.
- ✅ **Purpose**: The actual video player — full-featured UI, controls, playlists, recommendations, ads, chapters. Use when the user asks for a "video player"; use AdvancedVideo when they just need to display a video.
- ✅ **Import** (named, not default): `import { videoPlayer } from 'cloudinary-video-player'` and `import 'cloudinary-video-player/cld-video-player.min.css'`. **Do not use `dist/` in the CSS path** — use `cloudinary-video-player/cld-video-player.min.css` only.
404
-
- ❌ **WRONG**: `import cloudinary from 'cloudinary-video-player'` then `cloudinary.videoPlayer(...)` — use named `videoPlayer` instead.
405
-
- ✅ **player.source()** takes an **object**, not a string: `player.source({ publicId: 'samples/elephants' })`. ❌ WRONG: `player.source('samples/elephants')`.
406
-
- ✅ **Use refs**, not IDs: `const videoRef = useRef<HTMLVideoElement>(null)`; pass `videoRef.current` to `videoPlayer()`. Avoid `document.getElementById` with React (can cause DOM conflicts).
407
-
- ✅ **Store player in a ref**, not state: `const playerRef = useRef<ReturnType<typeof videoPlayer> | null>(null)`.
408
-
- ✅ **Race condition (ref/DOM)**: useEffect runs right after render; at that moment `videoRef.current` may still be **null** (ref not attached yet) or the element may not be in the DOM. **Wait until** `videoRef.current` is set and ready before calling `videoPlayer(videoRef.current, ...)`. Options: (1) small delay (e.g. `setTimeout(..., 0)` or 50ms) then check `videoRef.current`; (2) `useLayoutEffect` so ref is committed before running; (3) poll until `videoRef.current?.isConnected`. Otherwise: "Invalid target for null#on; must be a DOM node or evented object".
409
-
- ✅ **Initialize in useEffect** (or useLayoutEffect) with cleanup; **always dispose** in cleanup (wrap in try-catch so disposal errors don't throw).
410
-
- ✅ **Validate env** before init: if `!import.meta.env.VITE_CLOUDINARY_CLOUD_NAME`, set error state and return early.
Use when the user asks for a **video player** (styled UI, controls, playlists). For just **displaying** a video, use AdvancedVideo instead.
407
+
408
+
**Rule: imperative element only.** Do **not** pass a React-managed `<video ref={...} />` to the player — the library mutates the DOM and React will throw removeChild errors. Create the video element with `document.createElement('video')`, append it to a container ref, and pass that element to `videoPlayer(el, ...)`.
409
+
410
+
- **Package**: `cloudinary-video-player`. Install with `npm install cloudinary-video-player` (no version).
411
+
- **Import**: `import { videoPlayer } from 'cloudinary-video-player'` (named) and `import 'cloudinary-video-player/cld-video-player.min.css'` (no `dist/` in path).
412
+
- **player.source()** takes an **object**: `player.source({ publicId: 'samples/elephants' })`. Not a string.
- **If init fails** (CSP, extensions, timing): render **AdvancedVideo** with the same publicId. Do not relax CSP in index.html or ask the user to disable extensions.
- ✅ Use `placeholder()` and `lazyload()` plugins together
552
554
- ✅ Always add `width` and `height` attributes to `AdvancedImage`
553
555
- ✅ Store `public_id` from upload success, not full URL
554
-
- ✅ Always dispose video player in useEffect cleanup
556
+
- ✅ Video player: use imperative element only; dispose in useLayoutEffect cleanup and remove element with `if (el.parentNode) el.parentNode.removeChild(el)`
555
557
- ✅ Use TypeScript for better autocomplete and error catching
556
558
- ✅ Prefer `unknown` over `any` when types aren't available
- ✅ **Wait for script**: Before calling `window.cloudinary.createUploadWidget(...)`, ensure `typeof window.cloudinary?.createUploadWidget === 'function'`. If not ready, poll (e.g. setInterval until it exists) or inject the script in code and call createUploadWidget in the script's `onload`. Don't assume `window.cloudinary` means the API is ready.
666
668
- ✅ See PATTERNS → Upload Widget Pattern ("Race condition") and Project setup → Upload Widget ("Wait for script").
667
669
668
-
### Video player: "Invalid target for null#on; must be a DOM node or evented object"
669
-
- ❌ Problem: **Race condition** — useEffect runs right after render; at that moment `videoRef.current` may still be **null** (ref not attached) or the element may not be in the DOM. The player library requires a real DOM node.
670
-
- ✅ **Wait for ref/DOM**: Before calling `videoPlayer(videoRef.current, ...)`, ensure `videoRef.current` is set. Use a short delay (e.g. `setTimeout(..., 0)` or 50ms) then check `videoRef.current`, or use `useLayoutEffect`, or poll until `videoRef.current?.isConnected`. Clean up the timeout in useEffect cleanup.
671
-
- ✅ See PATTERNS → Cloudinary Video Player ("Race condition (ref/DOM)") and the example with `setTimeout(..., 0)`.
670
+
### Video player: "Invalid target for null#on" or React removeChild or NotFoundError
671
+
- ❌ Problem: Passing a React-managed `<video ref={...} />` to the player causes removeChild errors (the player mutates the DOM). Or container/ref not in DOM yet when init runs.
672
+
- ✅ **Use imperative video element only**: Create the video with `document.createElement('video')`, append to a container ref, pass that element to `videoPlayer(el, ...)`. Check `containerRef.current?.isConnected` before init. In cleanup: dispose, then `if (el.parentNode) el.parentNode.removeChild(el)`. See PATTERNS → Cloudinary Video Player (The Player).
673
+
674
+
### Video player: failed HEAD or CORS-like console noise
675
+
- Failed HEAD/analytics from the player does **not** necessarily mean playback fails. Do not add a preflight GET. If video doesn't play, use the imperative pattern and fall back to AdvancedVideo when init fails.
676
+
677
+
### Video player blocked by CSP or extensions
678
+
- **Do not** relax CSP in index.html or ask the user to disable extensions. **Fall back to AdvancedVideo** with the same publicId when the player fails to initialize.
672
679
673
680
### User needs secure/signed uploads
674
681
- ❌ Problem: User asks for secure uploads; unsigned preset or client-side secret is not acceptable.
4. If you need styled video player, use `cloudinary-video-player` instead
708
715
709
716
### "Video player not working" or "Player not initializing"
710
-
- ❌ Problem: Configuration or initialization issue with standalone player
711
-
- ✅ Solution:
712
-
1. Check `cloud_name` is provided in player config
713
-
2. Ensure CSS file is imported: `import 'cloudinary-video-player/cld-video-player.min.css'` (no `dist/` in path)
714
-
3. Verify player is initialized in `useEffect` with proper cleanup
715
-
4. Check video element has required classes: `cld-video-player cld-fluid`
716
-
5. Always call `player.dispose()` in useEffect cleanup function
717
-
6. For advanced features, ensure required modules are imported
717
+
- ✅ **Use imperative video element only** (see PATTERNS → Cloudinary Video Player): createElement, append to container ref, pass to videoPlayer; cleanup: dispose then `if (el.parentNode) el.parentNode.removeChild(el)`. If init still fails (CSP, extensions), **fall back to AdvancedVideo** with the same publicId. Do not relax CSP or ask the user to disable extensions.
718
+
719
+
### Cloudinary package install fails or "version doesn't exist"
720
+
- ❌ Problem: Agent pinned a Cloudinary package to a specific version (e.g. `cloudinary-video-player@1.2.3`) that doesn't exist on npm, or used a wrong package name.
721
+
- ✅ **Install latest**: Use `npm install <package>` with **no version** so npm gets the latest compatible. In package.json use a **caret** (e.g. `"cloudinary-video-player": "^1.0.0"`). Use only correct package names: `@cloudinary/react`, `@cloudinary/url-gen`, `cloudinary-video-player`, `cloudinary`. See PATTERNS → "Installing Cloudinary packages".
718
722
719
723
### Confusion between AdvancedVideo and Video Player
720
724
- **AdvancedVideo** = for **displaying** a video (not a full player). **Cloudinary Video Player** = the **player** (styled UI, controls, playlists, etc.).
### Video player: "source is not a function" or video not playing
739
-
- ❌ Problem: Using `player.source(publicId)` with a string.
740
-
- ✅ **player.source()** takes an **object**: `player.source({ publicId: 'samples/elephants' })`. Use named import: `import { videoPlayer } from 'cloudinary-video-player'` (not default `import cloudinary`). Use refs for the DOM element, not `document.getElementById`. See PATTERNS → Cloudinary Video Player (The Player).
743
+
- **player.source()** takes an **object**: `player.source({ publicId: 'samples/elephants' })`, not a string. Use named import: `import { videoPlayer } from 'cloudinary-video-player'`. See PATTERNS → Cloudinary Video Player (The Player).
741
744
742
745
### Overlay: "Cannot read properties of undefined" or overlay not showing
743
746
- ❌ Problem: Wrong overlay API usage (Overlay.source, compass constants, .transformation().resize, fontWeight on wrong object).
@@ -813,16 +816,14 @@ When something isn't working, check:
813
816
- [ ] Plugins are in array format
814
817
- [ ] Upload widget script is loaded in `index.html`
815
818
- [ ] **"createUploadWidget is not a function"?** → Wait until `typeof window.cloudinary?.createUploadWidget === 'function'` before calling it (script loads async; poll or use script onload)
816
-
- [ ] **Video player "Invalid target for null#on"?** → Wait for ref/DOM before calling videoPlayer (e.g. setTimeout 0 or 50ms, or useLayoutEffect); clean up timeout in useEffect cleanup
819
+
- [ ] **Video player?** → **Imperative element only**: createElement('video'), append to container ref, pass to videoPlayer(el, ...); player.source({ publicId }); cleanup: dispose then if (el.parentNode) el.parentNode.removeChild(el). CSS: cloudinary-video-player/cld-video-player.min.css. If init fails, fall back to AdvancedVideo (do not relax CSP).
817
820
- [ ] **Upload fails (unsigned)?** → Is `VITE_CLOUDINARY_UPLOAD_PRESET` set? Preset exists and is Unsigned in dashboard?
818
821
- [ ] **Secure uploads?** → Use `uploadSignature` as function (not `signatureEndpoint`); fetch `api_key` from server first; include `uploadPreset` in widget config; server includes `upload_preset` in signed params; use Cloudinary Node SDK v2 on server; never expose or commit API secret
819
822
- [ ] **Where do API key/secret go?** → **Do not** put in root `.env`. Use **`server/.env`**; add to `.gitignore`; load only in server. **Never commit** API key or secret
- [ ] **Video player?** → Use named import `videoPlayer` from `cloudinary-video-player`; `player.source({ publicId })` (object, not string); use refs; dispose in cleanup with try-catch; CSS: `cloudinary-video-player/cld-video-player.min.css`
824
+
- [ ] **Installing Cloudinary packages?** → Install latest: use `npm install <package>` with no version; in package.json use caret (^) so npm gets latest compatible; do not pin to exact versions
822
825
- [ ] **Image overlays?** → Import `source` (not Overlay.source); `compass('south_east')` (strings with underscores); `new Transformation()` inside `.transformation()`; fontWeight on TextStyle, textColor on text source
823
826
- [ ] **Image gallery?** → Use responsive/lazyload/placeholder plugins; use sample list (samples/cloudinary-icon, samples/bike, samples/landscapes/beach-boat, samples/food/spices, etc.); assume samples might not exist; use onError; prefer uploaded assets
824
-
- [ ] Video player is disposed in cleanup (with try-catch)
825
-
- [ ] CSS files are imported for video player
826
827
- [ ] TypeScript types are properly imported
827
828
- [ ] Upload result types are defined (not using `any`)
828
829
- [ ] Environment variables are typed in `vite-env.d.ts`
0 commit comments