> Public copy of the ViewStream developer guide (also for AI agents). All guides: https://www.viewstream.co.il/developers/guide/index.md · API reference: https://api.viewstream.co.il/docs/

# ViewStream for WordPress

Publishes new ViewStream videos as WordPress posts, connects the site to a ViewStream account without copying keys,
and uploads videos from the browser straight to ViewStream storage. **Download:** https://www.viewstream.co.il/downloads/viewstream-wordpress.zip (WordPress 6.4+, PHP 8.1+).
Plugin source: `viewstream/`; build the zip with `bash integrations/wordpress/build.sh`.
WordPress.org: assets in `wporg-assets/` (SVN `/assets`, never inside the zip), submission and
release steps in `WPORG-SUBMISSION.md`.

> **TODO (Dmitry): confirm the WordPress.org account.** `readme.txt` says `Contributors: viewstream`, but the existing
> wordpress.org user `@viewstream` was registered in 2005 and may not be ours. Before submitting, log in as the
> account that will own the plugin and put exactly its username there (or ours once created) — see
> WPORG-SUBMISSION.md step 1. A Contributors name that is not a wordpress.org user is silently dropped; one that
> belongs to a stranger would show them as the author.

## Free account from the plugin (1.3.0)

Not connected → Settings → ViewStream shows **Create a free ViewStream account** (primary) next to "Already have an
account? **Connect to ViewStream**", plus what the permanent free plan includes (5 GB storage, 60 transcode
minutes/month, 100 GB traffic/month, no live channels). The button runs exactly the Connect flow
(`ViewStream_Connect::start()`: same per-user `state` + PKCE verifier, same `admin-post.php?action=viewstream_connect_callback`
callback and code exchange) but opens `<studio>/register?app=wordpress&site=<admin origin>&redirect_uri=<callback>&state&code_challenge&code_challenge_method=S256&lang=he|en`
instead of `/connect`; the pending state lives an hour for sign-up (10 minutes for Connect) because e-mail
verification takes time — Studio must keep the `code_challenge`/`state` at least that long. Studio keeps those parameters through sign-up and e-mail verification, then shows the same
consent page and redirects to the same callback → connected. The Studio address is the "Studio address" setting under
"Advanced" (default `https://studio.viewstream.co.il`). The Studio side (`/register` honouring these parameters) is
built separately; until it ships, the button lands on Studio's sign-up page and the person has to press Connect after
signing up.

## עברית — התקנה

1. בוורדפרס: תוספים ← הוספה ← העלאת תוסף ← בחרו את `viewstream-wordpress.zip` (הורדה: https://www.viewstream.co.il/downloads/viewstream-wordpress.zip) ← הפעלה.
2. אין חשבון? הגדרות ← ViewStream ← **פתיחת חשבון ViewStream חינם** (נרשמים, מאמתים את המייל, מאשרים וחוזרים
   מחוברים). יש חשבון? הגדרות ← ViewStream ← **התחברות ל-ViewStream**: נפתח ViewStream Studio, מתחברים, בוחרים את הארגון ורואים בדיוק
   אילו הרשאות האתר מקבל ← **אישור וחיבור**. חוזרים לוורדפרס מחוברים: נוצר מפתח API מוגבל בשם
   `WordPress: <האתר>` (מופיע ב-Studio ← אינטגרציות ← מפתחות API ואפשר לבטל אותו שם), וה-webhook נרשם אוטומטית.
   **ניתוק** מבטל את המפתח. (חלופה: „מתקדם” ← הדבקת מפתח API ידני.)
3. בחרו מה יוצר פוסטים (סרטונים שפורסמו, קליפים סופיים, כתבות AI שפורסמו, תוכניות לצפייה חוזרת) ואיך הם נשמרים
   (טיוטה כברירת מחדל, ממתין לסקירה או פרסום מיידי), סוג התוכן, קטגוריה, כותב ותצורת הנגן.
4. **העלאת וידאו** (מדיה ← ViewStream, או בתוך הבלוק): הקובץ עובר ישירות מהדפדפן לאחסון של ViewStream בחלקים של
   64MB — לא דרך שרת הוורדפרס — עם פס התקדמות והמשך העלאה אחרי תקלה. כשהסרטון מעובד נוצר פוסט (לפי ההגדרות) בשם
   מי שהעלה אותו.
5. בעורך: בלוק **סרטון ViewStream** עם חיפוש בספרייה והעלאה, או קוד קצר
   `[viewstream id="<id>" kind="vod|clip|live|programme"]`.

## English — setup

1. WordPress: Plugins → Add New → Upload Plugin → `viewstream.zip` → Activate.
2. No account yet? Settings → ViewStream → **Create a free ViewStream account** (sign up, verify the e-mail, approve,
   back connected). Existing account: Settings → ViewStream → **Connect to ViewStream**: Studio opens, you sign in, pick the tenant, see the exact scopes
   and approve. Back in WordPress the site is connected: a scoped key `WordPress: <site>` exists (Studio → Integrations
   → API keys, revocable there) and the webhook is registered. **Disconnect** revokes the key. A manual key is still
   possible under "Advanced".
3. Choose what creates posts and how (draft by default, pending, or published), post type, category, author, player.
4. **Upload video** (Media → ViewStream, or in the block): the browser sends the file straight to ViewStream storage
   in 64 MB parts, never through the WordPress server; progress bar, automatic part retries and "Resume upload".
   When the video is ready, a post is created (per the settings) by the uploader.
5. Editor: the **ViewStream video** block (library search + upload) or `[viewstream id="…" kind="…"]`.

## How it works

| Piece | What |
|---|---|
| Connect | OAuth-style authorisation code + PKCE (S256). The plugin keeps a random `state` + `code_verifier` for the admin (10 min), sends the browser to `<studio>/connect?app=wordpress&site=<admin origin>&redirect_uri=<admin-post callback>&state&code_challenge&code_challenge_method=S256&lang`. Studio's consent page calls `POST /v1/connect/authorize` (a signed-in person with `keys:manage`, scopes ⊆ theirs) → 60-second one-time code. The callback checks `state` (single use, per user) and exchanges the code server to server at `POST /v1/connect/token` (verifier + identical redirect_uri) → the key, stored with autoload off. Disconnect = `POST /v1/connect/disconnect` with the key. |
| Upload | `POST /wp-json/viewstream/v1/upload/start` (`upload_files`) → the site calls `POST /v1/uploads` with its key and `browser_origin` = its admin origin → only `upload_token` + part URL go to the browser → `PUT <api>/v1/upload-parts/{n}?t=<token>` (CORS for that origin only), 3 parallel, 4 tries each, progress kept in localStorage (resume within the token's hour) → `POST /wp-json/viewstream/v1/upload/complete` → the site completes with its key (`publish: auto`). |
| Post on ready | Uploads are remembered (`viewstream_uploads`, autoload off); the `asset.ready` webhook or the 15-minute job imports them (even when "Published videos" is off), written by the uploader. |
| Webhook | `POST /wp-json/viewstream/v1/webhook` — `X-VS-Signature: sha256=HMAC-SHA256(secret, "<X-VS-Timestamp>.<raw body>")`, 300 s window, constant-time compare, deduplicated by `X-VS-Delivery`. Events: `asset.published` / `asset.ready` (published), `clip.final`, `artifact.published`. The body is only a trigger; the item is fetched from the API. |
| Poll | WP-Cron every 15 min: newest ready+published assets, final clips per channel, published AI articles, finished catch-up programmes. Only items newer than when polling was switched on. |
| Posts | Block `viewstream/video` + body; featured image from the poster (only from the tenant's CDN / ViewStream hosts); tags from topics; `_viewstream_key` meta makes every import idempotent. |
| Embed | iframe `https://player.viewstream.co.il/e/<tenant>/<vod|clip|live>/<id>` or the player tag; programmes and vertical clips use the player tag. |

## Accounts with playback protection

With token protection the tenant's policy decides where the player may run. A customer's site must be added in
Studio → Delivery → Protection, or its posts show no video:

- iframe embed (default): **Sites allowed to embed the player** — the hosted `/e/` page sends
  `Content-Security-Policy: frame-ancestors` with only these sites, so the browser refuses the frame elsewhere.
- player tag: **Allowed referrer sites** (the issuer answers 403 `referer` → "This video is not available on this
  site") and **Allowed player origins (CORS)** (else the browser blocks the HLS requests).

Settings → ViewStream shows the exact host to add. Posters under a protected path also need a token, so imported
posts get no featured image on such a tenant (platform gap, 2026-10-08).

## FAQ

- **Do I need an account?** Yes — the plugin is a ViewStream client — but it can be created for free from the plugin
  itself (Settings → ViewStream → **Create a free ViewStream account**). The free plan has no time limit: 5 GB of
  storage, 60 transcoding minutes and 100 GB of traffic a month, no live channels. Nothing is sent to ViewStream
  before an administrator presses that button or Connect.
- **צריך חשבון?** כן, אבל פותחים אותו בחינם מתוך התוסף (הגדרות ← ViewStream ← **פתיחת חשבון ViewStream חינם**).
  החבילה החינמית אינה מוגבלת בזמן: ‎5GB אחסון, 60 דקות קידוד ו-100GB תעבורה בחודש, בלי ערוצים חיים.

## Tested (1.3.0, 2026-10-08)

WordPress 7.1.3 / PHP 8.3 (docker on vs-dev-01, installed from the 1.3.0 zip): "Create a free ViewStream account"
sends the browser to `https://studio.viewstream.co.il/register?app=wordpress&site=…&redirect_uri=…admin-post.php?action=viewstream_connect_callback&state=…&code_challenge=…&code_challenge_method=S256&lang=en|he`
and Connect to `/connect` with the same parameter set; a forged `state` on the shared callback is refused with the
notice. Settings, library and block in English and Hebrew against a local mock API (the WordPress.org screenshots).
Plugin Check 2.1.0 (with experimental checks): no errors. PHPCS WordPress 3.4.1: clean; PHPCompatibilityWP (8.1+):
clean. WordPress.org readme validator (meta.svn `plugin-directory/readme`, run in WP): no errors, no warnings.
Not tested: the real sign-up round trip (Studio `/register` is being built).

## Tested (1.2.0, 2026-10-08)

WordPress 7.1.3 on PHP 8.5 and 8.4, WordPress 6.4.3 on PHP 8.1 (docker, installed from the zip), against the
production API with a manual key: settings (English and Hebrew), library search, block and shortcode render, poll
→ draft posts, uninstall (also multisite). Plugin Check 2.1.0 (with experimental checks): no errors.
PHPCS WordPress 3.4.1 standard: clean; PHPCompatibility 10 (8.1+): clean.

## Security

- Connect: PKCE S256 and `state` mandatory; redirect_uri must be on the site's own origin (https; plain http only for
  localhost), no user info / fragment; codes 60 s, single use (a wrong verifier burns the code), stored as SHA-256;
  the token endpoint is rate limited (10/min per IP); only a person (never an API key) can approve, and only scopes
  they hold. Studio's consent page cannot be framed (`frame-ancestors 'none'`).
- Uploads: the browser never sees the API key; the upload token allows only part PUTs of one upload until it expires
  and dies when the key is revoked; CORS answers only the connected origin; completing still needs the key.
- Capability checks and nonces on every admin action; the API key, webhook secret and upload list are separate
  options with autoload off, never printed back. No third-party libraries.
- `uninstall.php` removes every option, transient and scheduled event of the plugin (posts stay).

## Production verification after the control-plane release

1. Install the zip on a test WordPress over HTTPS → Settings → ViewStream → **Connect to ViewStream** → approve in
   the `demo` tenant → expect "Connected to …", a `WordPress: <host>` key in Studio → Integrations, a webhook.
2. Media → ViewStream → upload a small video (≥ 65 MB to get 2 parts) → watch DevTools: parts go to
   `https://api.viewstream.co.il/v1/upload-parts/…` (no `Authorization` header), 200 + `ETag` → asset in the library,
   draft post once ready.
3. **Disconnect** → the key shows as revoked in Studio.
4. (once Studio `/register` ships) **Create a free ViewStream account** with a fresh e-mail → verify → consent →
   back in WordPress "Connected to …" on the free plan; upload a small video.

## Development

- Strings: `python3 integrations/wordpress/tools/l10n.py` regenerates `languages/` (pot, Hebrew po/mo, the JSON of
  both scripts); `--check` fails on a missing Hebrew translation.
- Zip: `bash integrations/wordpress/build.sh` → `integrations/wordpress/viewstream.zip`.
- `release/0080-dryrun.sql`: migration 0080 + EXPLAIN of every new statement inside `BEGIN … ROLLBACK`.
- WordPress.org assets: `tools/wporg-assets-src/` (banner.html, icon.svg, render.js) →
  `node tools/wporg-assets-src/render.js tools/wporg-assets-src wporg-assets <playwright-core path>` (headless Chrome;
  vs-dev-01 has both). Screenshots were taken on a throw-away WordPress with a local mock API (neutral sample titles —
  never customer footage). The older `screenshots/` folder (Hebrew, real broadcaster frames) is for internal docs
  only; do not upload it to WordPress.org.
