Bitflix

Live source: recommendations.md on GitHub · Auto-rebuilt on every streaming deploy-web.

Bitmovin product & DX feedback — from a coding agent's first pass

Built on Bitmovin Encoding (Per-Title + Encoding Templates), Bitmovin AI Scene Analysis, Bitmovin Stream Lab, Bitmovin Player + Analytics on AWS S3 + CloudFront, with a browser-use E2E test. End-to-end pipeline: 10 titles, ingest → encode → AI → manifests → Stream Lab → deploy → live verification at https://bitflix.slederer.com/.

This file captures every place an agent / engineer hits friction and what would have shaved iterations. Severity is by minutes-of-loop-time it cost an autonomous agent on a single build: - P0 — broke the build with no actionable error. - P1 — required reverse-engineering a schema or a multi-doc-fetch detour. - P2 — minor papercut.

Every item is paired with the actual error message I hit, and a one-line suggested fix.

This document is updated continuously — every time a new DX issue surfaces during development, it goes here.


Successes worth keeping (so the rest of the file isn't all complaints)


P0 — broke the build with no actionable error

1. Encoding Templates expect YAML body, but the Python SDK ships JSON

api.encoding.templates.start({"metadata": {...}, ...})
# → HTTP 500
# {"data":{"code":1004,"message":"An unexpected error occurred",
#  "developerMessage":"Please contact support and provide the requestId"}}

The SDK accepts a dict, calls to_dict(), sends Content-Type: application/json. The API silently 500s. Only path that works:

requests.post(API + "/encoding/templates/start",
              headers={"X-Api-Key": ..., "Content-Type": "application/yaml"},
              data=rendered_yaml.encode("utf-8"))

Cost: ~20 min of debugging an opaque 500. Fix (SDK): templates.start() should serialize as YAML by default, or accept a format= kwarg. Fix (API): A 415-Unsupported-Media-Type with a Accept: application/yaml hint would have been instantly actionable. Fix (Docs): The Encoding Templates page should call out that the body must be YAML, not JSON.

2. Encoding Template references — three different syntaxes, only one works

Tried in order:

Attempt Field Value Result
1 codecConfigName h264_per_title "Got unexpected field. Fields: [codecConfigName]"
2 codecConfigId h264_per_title "Codec configuration with id 'h264_per_title' not found in the system"
3 codecConfigId $/configurations/video/h264/h264_per_title

The same $/path rule applies to inputId, outputId, streamId, manifestId. The docs page (developer.bitmovin.com/encoding/docs/encoding-templates) hints at the syntax via one phrase ("references … such as inputs, outputs and configurations all being used in the encoding section") but never spells it out. The JSON schema's string type for these fields gives no clue.

Cost: ~30 min of trial-and-error per encoding submitted (each iteration costs an actual encoding job). Fix (Docs): Single highlighted box: "All cross-references in templates use JSON-pointer paths starting with $/." Plus one example per resource type. Fix (API): When a bare ID is given for a known field, return 4xx with: "For Encoding Templates, use $/configurations/video/h264/h264_per_title instead of bare ID."

3. segment_{number}.m4s silently overwrites every segment

Encoding Template format does not substitute {number} in segmentNaming or outputPath. The token is treated as a literal filename. Every audio segment writes to audio/segment_{number}.m4s and the verification pass fails:

- Upload verification failed for audio segment 0.
- Encoding has failed.

Default segment naming works fine; any custom value produces this. No warning at submission time.

Cost: 1 wasted encoding cycle on all 10 titles. Fix (Encoding service): At template parse time, reject {/} in segmentNaming unless preceded by a known-substitution token (%number% or whatever the canonical placeholder is). Fix (Docs): One sentence in the muxing reference: "Bitmovin auto-generates segment names; do not include {number} in segmentNaming — it is treated as a literal."

4. Encoding-template manifests + per-title = "manifests removed due to Stream Conditions"

- The following HLS Manifests were removed due to Stream Conditions: 433ffae1-...
- The following DASH Manifests were removed due to Stream Conditions: ff9483ff-...
- Encoding has finished successfully.

Encoding finished, segments uploaded, sprites generated, AI scenes.json written — but no master.m3u8 / stream.mpd. The HLS/DASH manifest blocks in the template need explicit streams.<id>.{encodingId, streamId, muxingId} references. For per-title encoding, those stream IDs don't exist until after the analysis phase. So template-side manifests are practically incompatible with per-title.

The only working path I found: drop manifests + start.vodHlsManifests/vodDashManifests from the template entirely, then call api.encoding.manifests.hls.default.create + api.encoding.manifests.dash.default.create after the encoding finishes. Nowhere documented.

Cost: ~40 min reverse-engineering, ~$5 of doomed encoding minutes. Fix (Encoding service): When streams is absent on a template manifest and the encoding has per-title muxings, auto-generate from the muxings. (This is exactly what the default-manifest API already does — bring it inside the template execution path.) Fix (Docs): Big call-out box: "For per-title encodings, do not declare manifests in the template — use the default-manifest API after the encoding finishes."

5. s3:GetBucketLocation is undocumented but mandatory

The IAM policy example in the encoding-customers / S3 input docs lists only s3:GetObject, s3:ListBucket, s3:PutObject. Without s3:GetBucketLocation (and ideally s3:ListAllMyBuckets):

- Encoding Template error for resource: $/outputs/s3/bitflix_output:
  Unable to query location of the bucket.
  Please verify secret key, access key, bucket name and permissions.

The error language ("Please verify secret key, access key…") points the user at credentials, but the actual missing permission is on the bucket-discovery side. We did have valid keys.

Cost: 15 min of suspecting the access key/secret were wrong. Fix (Docs): Update the canonical IAM policy example to include s3:GetBucketLocation (+ s3:ListAllMyBuckets). Fix (Error message): When the credentials are valid but a GetBucketLocation AccessDenied is the actual cause, the error should say so.

6. AI Scene Analysis fails the whole encoding on shorter content

Titles under ~5 minutes (Agent 327: 3:51, Spring: 7:40 — and Spring still failed) trigger:

- AI scene analysis in progress.
- AI scene analysis failed.
- Encoding has failed.

There's no way to find out why the AI failed (no detail, no retry-hint), no documented minimum duration, no onError: SKIP flag to keep the H.264/AAC/HLS/DASH outputs even when AI bailed.

For a feature being marketed heavily right now ("VOD Workflows", "IBC 2025"), gating an entire encoding on a brittle, undocumented preview makes adoption hard.

Cost: 2 wasted encoding cycles, ~25 min. Fix (Product): Per-feature onError: SKIP | FAIL knob. Default SKIP for AI, FAIL for transcoding. Fix (API): Surface the actual AI error reason (unsupported codec? duration? content?) instead of a single-line "failed". Fix (Docs): Document the minimum duration and supported formats explicitly. The blog posts say "VOD-first"; the API enforces undocumented constraints.


P1 — required reverse-engineering a schema or multi-doc detour

7. Stream Lab API request body is undocumented and wrong-shaped in every blog post

What I tried first (matches every public blog snippet):

POST /v1/player/testing/streams
{"type": "HLS", "url": "https://...", "title": "..."}
→ 400 "config should not be null or undefined"

Reverse-engineering through 5 progressive 400 responses revealed the actual schema:

{"config": {"name": "...",
            "streamType": "hls",
            "streamDrmType": "none",
            "streamUrl": "https://...",
            "version": 1}}

streamType must be lowercase ("HLS" is rejected). streamDrmType: "none" is required even for unprotected content. version: 1 is a magic constant.

Cost: 25 min, 6 wasted submissions across all titles. Fix (Docs): A complete example body on the Stream Lab API docs page (/playback/docs/streamlab-api). The page currently describes capabilities but never shows a body. Fix (API): version shouldn't be required if there's only ever been one version.

8. Stream Lab tests submit successfully but device-matrix results aren't exposed via the API

The dashboard at https://dashboard.bitmovin.com/player/stream-lab shows beautiful per-device pass/fail with screenshots and waterfall logs. The corresponding API GET /v1/player/testing/streams/<id> returns:

{"id": "...", "config": {...}, "suiteId": "...", "createdAt": "...", "updatedAt": "..."}

No status, no results, no devicesPassed, no link to a results subresource.

For a CI gate, this is the most-requested capability of Stream Lab — and it doesn't exist in the public API.

Cost: I had to drop the gating step and switch to "submission gate only" mode. Fix (API): Expose the device-matrix outcome via GET /v1/player/testing/streams/<id>/results. Even a JSON dump of what the dashboard shows would be ideal.

9. Encoding Template JSON schema lives in a different repo from the docs

Authoritative reference: https://raw.githubusercontent.com/bitmovin/bitmovin-api-sdk-examples/main/bitmovin-encoding-template.json — a 17,430-line JSON schema. This is what an agent has to download and parse to discover that: - streams.<id> has sub-keys properties, filters, burnInSubtitles, watermarking, captions, bifs, hdr, thumbnails, sprites - start.aiSceneAnalysis.features exists with assetDescription + automaticAdPlacement - manifests is top-level only (not under encodings.<id>) - streams.<id>.properties.mode enum values are STANDARD | PER_TITLE_TEMPLATE | PER_TITLE_TEMPLATE_FIXED_RESOLUTION | PER_TITLE_TEMPLATE_FIXED_RESOLUTION_AND_BITRATE | PER_TITLE_RESULT

Cost: 35 min to script-mine the schema for the structure I needed. Fix (API): Serve the schema at GET /v1/schemas/encoding-template.json so SDKs and agents can validate locally. Fix (Docs): The encoding-templates docs page should link prominently to the schema. Right now it doesn't.

10. AI Scene Analysis output schema is undocumented

The assetDescription feature writes a scenes.json like this:

{"scenes": [
  {"title": "Enchanted Animated Landscape and Singing Bird",
   "startInSeconds": 0.0, "endInSeconds": 25.42,
   "id": "...",
   "content": {
     "characters": [{"appearance": "...", "name": "Unknown", "description": "..."}],
     "objects": [{"description": "...", "category": "Nature"}],
     "dialogue": [...]
   }}
]}

No documentation describes this shape. The marketing blog says scene analysis produces "metadata" — devs have to inspect the JSON to know there's a content.characters[], etc. The structure is genuinely useful (vs. just timestamps) and would drive product adoption if showcased.

Cost: 5 min reverse-engineering schema while debugging player.js. Fix (Docs): A "scenes.json" reference page with field definitions. Today there is none.

11. automaticAdPlacement.schedule requires non-zero positions but the docs example has all zeros

I tried {"position": 0.0, "maxDeviation": 0.0, "duration": 0.0} — based on a "minimal example" mental model. Encoding errored. Removing the block fixed it. The docs offer no working example, no defaults, and no clear error.

Fix (API): Reject duration: 0 at template-parse time with duration must be positive. Fix (Docs): One worked example: pre-roll only [{"position": 0.0, "duration": 30.0, "maxDeviation": 0.0}].

12. start.encodingMode: STANDARD conflicts with presetConfiguration: VOD_STANDARD

EncodingMode cannot be set on the start request and on a codec
configuration at the same time. Note that setting presetConfiguration
on a codec configuration will implicitly set its EncodingMode

The error message is great. The root cause is that two paths exist for setting the same thing — they shouldn't both be authoritative. Pick one.

Fix (API): Deprecate start.encodingMode when presetConfiguration is set; warn rather than error.

13. selectionMode: AUTO + position: 0 produces a warning but is also the SDK example default

Check inputStreams of Stream f540da8a-...: "position" should not be set
when using selectionMode "AUTO". Position (0) will be ignored.

Many bitmovin-api-sdk-examples Python snippets include position=0 even with AUTO. Either remove position from the AUTO examples, or stop emitting the warning when position == 0.

14. Encoding messages are returned unsorted, so the "last error" isn't the most-recent

for m in s.messages or []:
    print(getattr(m,'text', m))
# - Encoding has failed.
# - Analyzing input finished.
# - AI scene analysis failed.
# - Download of input file finished.
# ...

Order is approximately random. Tooling has to sort by date to get a chronology, and even then date granularity is per-second so multiple events collide. Building any kind of "what was the proximate cause" UI/agent on top is painful.

Fix (API): Either return messages sorted ascending by event time, or include a phase enum (DOWNLOAD | ANALYSIS | TRANSCODE | MUX | MANIFEST | AI | UPLOAD | VERIFICATION) so callers can group.

15. SDK package version conflicts on Python <3.10

bitmovin-api-sdk versions ≤ 1.234 pin requests<2.25.0 — incompatible with anything modern (boto3 ≥ 1.34, responses ≥ 0.25). On macOS system Python 3.9, dependency resolution is impossible. Forcing bitmovin-api-sdk ≥ 1.260 requires urllib3 ≥ 2, which on Python 3.9 conflicts with botocore. End result: users must install Python 3.11+ to use both the Bitmovin SDK and AWS together — but neither the SDK README nor the Encoding docs mention this.

Fix (SDK): Drop the requests<2.25.0 pin in older releases via a yanked-and-republished wheel, or document a clear minimum Python version.

16. Default-manifest output path must be ancestor of muxing paths — undocumented

2701: Muxing output path is not a sub path of the default manifest output path!

Hit this when I tried to put master.m3u8 at vod/<slug>/hls/ while video muxings were at vod/<slug>/video/<rung>/. The fix is to put the manifest at vod/<slug>/. Not documented anywhere.

Fix (Docs): One sentence in the default-manifest reference: "The manifest's outputPath must be an ancestor of every muxing's outputPath so relative URIs can be computed." Fix (API): A clearer error: "Muxing output 'vod/foo/audio/' is not under manifest output 'vod/foo/hls/'. Move the manifest to 'vod/foo/' or move the muxing under 'vod/foo/hls/audio/'."


P2 — papercuts


Agent / coding-tool friendliness — recommendations

These four changes specifically would make Bitmovin the easiest streaming platform in the market for AI coding agents, which is likely a fast-growing audience:

A. Self-validating templates

POST /v1/encoding/templates/validate
Body: <YAML>
→ 200 { "valid": true, "warnings": [...] }
→ 400 { "errors": [{ "path": "encodings.foo.streams.bar.properties.codecConfigId",
                     "message": "Expected '$/configurations/...' path, got 'h264_per_title'",
                     "suggestion": "$/configurations/video/h264/h264_per_title" }] }

This single endpoint would have eliminated ~5 of my 7 wasted encoding cycles. It's also a strong signal to agents that Bitmovin is a "modern" platform.

B. Machine-readable error responses

Every 4xx/5xx today has a requestId, developerMessage, and a free-text message. Add:

{
  "errors": [
    {"path": "encodings.bitflix_encoding.streams.video_stream.properties.codecConfigId",
     "code": "REFERENCE_NOT_FOUND",
     "message": "Codec config not found",
     "expectedFormat": "$/configurations/video/<codec>/<id>",
     "got": "h264_per_title",
     "suggestion": "$/configurations/video/h264/h264_per_title"}
  ]
}

The current "An unexpected error occurred — Please contact support" pattern actively prevents self-healing automation.

C. SDK accessor for AI Scene Analysis

bitmovin-api-sdk-python 1.264 has no api.encoding.encodings.machine_learning.* or api.ai.scene_analysis.*. Heavy marketing investment + zero SDK surface = agents have to reverse-engineer from blog posts. A simple wrapper:

api.encoding.ai.scene_analysis.run(encoding_id, features=...)
api.encoding.ai.scene_analysis.status(analysis_id)
api.encoding.ai.scene_analysis.results(analysis_id)

Would close the marketing-vs-product gap dramatically.

D. A "starter pack" YAML repo with one file per common workflow

bitmovin-encoding-templates-cookbook/
  vod/
    per-title-h264-aac-hls-dash.yaml          # the most common case
    per-title-with-sprites.yaml
    per-title-with-ai-scene-analysis.yaml
    per-title-with-drm-widevine.yaml
    multi-drm-fairplay-widevine-playready.yaml
    captions-from-source.yaml
    ssai-mediatailor.yaml
  live/
    rtmp-input-to-hls-dash.yaml
    low-latency-cmaf.yaml
  utils/
    ingest-aspera.yaml
    notification-webhooks.yaml

Every file complete, runnable, with parameters. The current bitmovin-api-sdk-examples repo has Python scripts that call the imperative API — that's not the same audience as someone authoring an Encoding Template.


TL;DR — five changes that would 10× the agent / DX experience

  1. Ship a /encoding/templates/validate dry-run endpoint (P0 → Day-1 productivity)
  2. Add structured-error responses with path + expectedFormat + suggestion (P0 → makes agents self-heal)
  3. Default-manifest path constraint + AI minimum duration: document or auto-fix (P1 → eliminates the most common surprise failures)
  4. Stream Lab device-matrix results in the public API (P1 → makes Stream Lab CI-able, the actual point)
  5. A "cookbook" YAML repo: one canonical file per common workflow (P1 → time-to-first-success drops from hours to minutes)

The product is genuinely capable. The friction is almost entirely in the surface area: error messages, schema discovery, SDK gaps, and missing example coverage. Closing those gaps doesn't require new features — it requires polishing the ones you already have.

— Generated while building Bitflix on bitmovin.com, 2026-04-26.


Update — post-launch friction (2026-04-26, second session)

Stefan reported four issues that "passed" my automated E2E test but were broken in real use. Every one of them is a Bitmovin DX issue that warrants its own fix.

17. playerCfg.ui = true silently destroys the entire UI but the player "loads"

[WARN] bitmovinplayer.js:17  Could not load UI
TypeError: Cannot create property 'metadata' on boolean 'true'
    at new e (bitmovinplayer-ui.js:17:40562)
    at new e.buildUI (bitmovinplayer-ui.js:17:232653)
    at Object.uiManagerFactory (bitmovinplayer.js:17:674194)

A typo / wrong type in playerCfg.ui causes the UI factory to crash, but the player core keeps loading. The user sees a black <video> element with no controls. My E2E test passed because: - <video> element existed - readyState === 4 (HAVE_ENOUGH_DATA) - An analytics POST fired

…but there were no Play/Pause buttons, no seekbar, no quality selector, no chapter markers. Visually broken; programmatically "passing."

Root cause inside the SDK: Cannot create property 'metadata' on boolean 'true' says the UI factory tries ui.metadata = ... where ui is the literal boolean true. Type was rejected internally but the player kept going.

Severity: P0 — silently broken UI is the worst kind of bug.

Fix (Player SDK): 1. The PlayerConfig.ui type should be false | UIConfig | undefined, not true | false | UIConfig | undefined. ui: true has no valid meaning (the default UI is what you get by omitting ui); reject it at construction time with a clear error. 2. UI factory failures should be errors, not warnings. The current console.warn is below default error filters and easy to miss. 3. Provide player.getUI() returning the loaded UI version / module so automated tests can verify "did the UI actually mount?" rather than just "is there a <video>?"

Fix (TypeScript types): This shouldn't compile in TS:

const cfg: PlayerConfig = { key: '...', ui: true };  // currently allowed

18. licenses.list()[0] defaults to "default-license" but the dashboard is filtered to a different one

Symptom Stefan saw: "in bitmovin observability/analytics, there are no impressions captured of the player."

What was actually happening:

Account analytics licenses (via api.analytics.licenses.list()):
  [0] default-license   (id: 2182cfa0-...)  impressions: 11   ← my pipeline used this
  [1] Stream Lab        (id: 52cbc695-...)  impressions:  0   ← Stefan's dashboard view

api.analytics.queries.count confirmed 11 impressions on default-license from this build. Stefan's dashboard was filtered to "Stream Lab" → empty result.

Severity: P0 for adoption — this is the kind of bug where someone gives up on the product thinking "Bitmovin Analytics doesn't work."

Fix (Dashboard): 1. When a license has 0 impressions but the account has impressions on other licenses, show a banner: "No impressions for this license. 11 impressions found on 'default-license'. [Switch view]" 2. The default dashboard view should be All licenses, not a single license. Multi-license filtering is an advanced filter, not a default.

Fix (SDK): 1. licenses.list() should expose is_default: bool or purpose: enum so SDK consumers can pick the right license deterministically. Today both licenses have order_index: 0; you can't tell them apart programmatically. 2. Provide api.analytics.licenses.get_default() that returns the canonical "main" license for the account. 3. Add licenses.list()[i].dashboard_url so an SDK can print a clickable link to the right dashboard view.

Fix (Setup-keys flow): When fetching the "first" license, warn loudly if the account has multiple. My streaming setup-keys should have said:

Found 2 analytics licenses: default-license, Stream Lab. Picking 'default-license'. To use a different one, set BITMOVIN_ANALYTICS_LICENSE_KEY in .env.local.

19. Stream Lab streams persist via API but don't surface in dashboard until manually triggered

Stefan: "in the bitmovin portal I don't see that you executed a streamlab test."

Verified via API:

GET /v1/player/testing/streams  →  total: 21 streams persisted
GET /v1/player/testing/streams/<id>  →  {id, name, streamType, streamUrl, suiteId, createdAt, updatedAt}

All 21 streams share the same suiteId: 6f74025c-…. They're stored, but no test was actually executed — the API calls submission "creates a stream record"; running tests against the device matrix is a separate (apparently UI-only) action.

The Stream Lab API docs and blog posts ("submit your stream for testing on real devices") strongly imply that POSTing creates a test run. It does not. The dashboard has a "Run test" button per stream.

Severity: P1 — fundamental feature mismatch between the API name (/testing/streams) and what it actually does (saves a stream record).

Fix (API): 1. Add POST /v1/player/testing/streams/{id}/runs (or /test) that actually triggers a device-matrix run. Without this, Stream Lab is not CI-able. 2. Expose GET /v1/player/testing/streams/{id}/runs returning per-device pass/fail. (Same complaint as #8 above.)

Fix (Docs): 1. The Stream Lab docs page must say explicitly: "Submitting a stream stores a stream definition. To run tests against it on the device matrix, use the dashboard or the Run-Test API." 2. The endpoint name is misleading. Consider renaming /v1/player/testing/streams/v1/player/testing/stream-defs, with a new /v1/player/testing/runs endpoint for actual test runs.

Not strictly a Bitmovin issue, but: the Bitmovin Player accepts source.poster: 'https://upload.wikimedia.org/...'. Wikipedia returns the image with cache headers but Cross-Origin-Resource-Policy: same-origin, so the browser drops the load (naturalWidth: 0). No console error, no Player-emitted warning.

Fix (Player SDK): 1. When source.poster fails to load, fire an ERROR event (or at minimum a WARN event) so the page can fall back. Today the failure is silent. 2. Better: in encoding output, automatically write a single-frame poster.jpg at the title root alongside the manifests. Default to vod/<slug>/poster.jpg when source.poster is missing. The trickplay sprite already contains this frame at (0,0,320,180); extracting it is trivial server-side.

Workaround I shipped: extracted the first 320×180 cell from each title's sprite-{number}-0.jpg with PIL, upscaled to 1280×720, uploaded to s3://bitflix-slederer-output/vod/<slug>/poster.jpg.

21. Sprite filename sprite-{number}-0.jpg keeps {number} literal — same bug as segments

I removed segmentNaming from the muxing block (per item #3 above) but the sprite output files came out named:

vod/<slug>/trickplay/sprite-{number}-0.jpg     (literal {number}!)

I never set spriteName. Bitmovin's default sprite naming includes a {number} placeholder that doesn't get substituted in Encoding Templates. Same root cause as segment naming.

Severity: P1 — confusing, but unique sprites are still produced (1 file per encoding) so it's mostly cosmetic. Trickplay still works because the WebVTT thumbnails.vtt references the literal filename.

Fix (Encoding service): substitute {number} in default sprite naming, or change the default to a name without placeholders (e.g., sprite-0.jpg, sprite-1.jpg, …).

22. GET /v1/analytics/impressions returns 500 with "An internal server error occured" (typo)

HTTP 500
{"requestId":"f829c973-…","status":"ERROR",
 "data":{"code":1900,"message":"Error processing the request!",
         "developerMessage":"An internal server error occured"}}

The POST /v1/analytics/queries/count and POST /v1/analytics/impressions endpoints work; the GET variant 500s. Also the developer message has a typo (occuredoccurred).

Fix: implement or remove the GET variant. Fix the typo.

23. Player UI loaded vs not-loaded is visible only via document.querySelector('.bmpui-ui-uicontainer')

There's no public API on the Player instance to ask "did the UI mount successfully?" My E2E now checks for the CSS class .bmpui-ui-uicontainer — fragile, will break if Bitmovin changes the class name.

Fix (Player SDK): Add player.isUIReady() → bool and an ON_UI_READY event.


Updated TL;DR — top fixes after one full build cycle

In order of impact for adoption + DX:

  1. playerCfg.ui should reject true at construction — silently breaking the UI is unforgivable. (Item #17)
  2. POST /encoding/templates/validate dry-run endpoint — kills 90% of iteration cycles. (Item original-1)
  3. Stream Lab /streams rename + new /runs endpoint that actually triggers device-matrix tests — fixes the fundamental mismatch between the API and what users expect. (Item #19)
  4. Multi-license analytics dashboard default to "All licenses" + SDK is_default flag — eliminates the "no data showing" support tickets entirely. (Item #18)
  5. Structured-error responses with path + expected + suggestion — makes agents self-heal. (Item original-2)
  6. One canonical YAML cookbook for the 10 most common workflows — turns hours of trial-and-error into minutes. (Item original-D)
  7. Default-manifest path constraint + AI Scene Analysis minimum duration: document or auto-fix. (Items original-3, original-6, item #16)
  8. Player events for source.poster load failures + ON_UI_READY event — makes E2E tests catch real failures. (Items #20, #23)

Meta-observation

Of the 23 friction items above, only ONE was a feature gap. Every other item was a polish issue: error messages, schema docs, SDK ergonomics, default behavior. The product is genuinely strong (per-title encoding, AI Scene Analysis, Player+Analytics) — and that's exactly why these papercuts are worth fixing. Right now Bitmovin is a great product wrapped in a thin layer of "you have to know the right answer to use it."

For AI coding agents specifically (which are increasingly the audience): the silent-success failure modes (Item #17, #20) and the dashboard-vs-API mismatches (Item #18, #19) are catastrophic. Agents validate by automated tests; if those tests pass while the product is broken, they ship broken products. Bitmovin's competitors will start advertising "agent-friendly" support — make sure that's Bitmovin first.

— Updated 2026-04-26, second session.


Update — third session: Stream Lab API surface + AI implicit-run investigation (2026-04-26)

~~24. Stream Lab has NO public API~~ → RETRACTED. I missed the documented endpoints.

Original claim was wrong. The endpoints exist and are documented at https://developer.bitmovin.com/playback/docs/streamlab-api. I did not find them on my first probe because I tried sibling paths like /streams/{id}/run and /jobs, but the actual run-trigger endpoint requires going through /streams/{id}/targets/{streamTargetId}/jobs — a 4-segment URL that I'd never have guessed without the docs.

Confirmed working endpoints (verified live, 2026-04-26):

Verb Path Result
GET /v1/player/testing/targets ✅ Returns 31 device targets (Chrome/Edge/Firefox on macOS/Windows/Linux, Safari, LG WebOS 2016–2024, Samsung Tizen 2016–2024, Vizio Smartcast, Xbox Series S, PlayStation 5)
GET /v1/player/testing/vpn-locations ✅ Returns 79 geographic locations (50+ countries: Albania → Ukraine, multi-city for Australia/UK/USA/Singapore/Italy/Germany/Netherlands)
GET /v1/player/testing/limits ✅ Returns {monthlyTestExecutionsLimit, activeStreamTargetsLimit, monthlyTestExecutions, activeStreamTargets, hasActiveAnalyticsSubscription}
POST /v1/player/testing/streams/<id>/targets Body: {targetId, vpnLocationId?, automaticTestEnabled?, drmHeaders?} — links a target to a stream
POST /v1/player/testing/streams/<id>/targets/<streamTargetId>/jobs Triggers a manual test execution
GET /v1/player/testing/jobs/<jobId>/testresults Returns per-device results (startup time, bitrate, errors)

This is a genuine feature, not a gap. Stream Lab is fully CI-able via the public API today.

What changes about the recommendations:

The endpoint exists, but it took a CEO sending me back to the docs to find it. That tells me there is still a real DX issue here — but it's discoverability, not absence:

  1. The endpoint name /streams/{id}/targets/{streamTargetId}/jobs is unguessable. Most APIs put the action verb in the path (/streams/{id}:test, /streams/{id}/run). Stream Lab models device-test as a relationship between stream + target, then jobs against that relationship. That's a defensible RESTful design but it's not where an agent would look first.

  2. First-pass probing won't find it. My initial for verb in ["test","run","runs","start","execute"] loop missed it entirely because there's no single-segment shortcut. An LLM agent given just the URL /v1/player/testing/streams is unlikely to explore four levels deep.

  3. The docs page (https://developer.bitmovin.com/playback/docs/streamlab-api) DOES describe all of this. I missed it because I was searching the page for "trigger" / "execute" / "run" — the docs use "create job" terminology. The page itself reads as a reference, not a quickstart, so the path streams → targets → jobs is implicit not foregrounded.

Suggested fixes: 1. Add a "Quickstart" code block at the top of the Stream Lab docs page showing the 4-call sequence: list targets → create stream → link target → create job → fetch results. Five copy-pasteable cURLs. 2. In the SDK: collapse the four-call sequence into one helper: api.player.testing.run_test(stream_url, type_, target_name="Chrome on Ubuntu Linux", vpn_location_name="Germany Frankfurt") → TestResult. 3. Endpoint discoverability: include a _links HATEOAS section on the GET /streams/{id} response pointing at the available actions (targets, targets/{tid}/jobs). 4. /limits endpoint should be in the SDK: surface monthly_test_executions_remaining so callers can budget. (My probe used 0/20; one full run of 20 tests will exhaust the quota for the month.)

Mea culpa: my error here is the most important data point in this whole document. If the CEO of Bitmovin had to redirect me to the docs to find a documented endpoint, every other coding-agent customer is going to make the same mistake. Treat agent-onboarding-time-to-first-test as a primary metric for the Stream Lab product.

25. AI Scene Analysis runs implicitly at the account level — undocumented behavior

I deliberately removed start.aiSceneAnalysis from my Encoding Template (per item #6 above). Bitmovin still produced rich scenes.json for 9/10 titles. Examples (live, on bitflix.slederer.com):

Title Runtime scenes.json source Scenes count
agent-327 3:51 Real Bitmovin AI 4
big-buck-bunny 9:56 Real 10
sprite-fright 10:15 Real 12
spring 7:40 Heuristic fallback 5
sintel 14:48 Real 10
plan-9-from-outer-space 1h 21m Real 47
night-of-the-living-dead 1h 36m Real 37

So: 1. AI Scene Analysis ran on every encoding even though I never asked for it. Account-level default is implicitly on for accounts that have AI enabled. 2. My "minimum duration" hypothesis from item #6 was wrong. The shortest title (3:51) succeeded; the failed one (Spring, 7:40) is mid-pack. 3. Spring failed because the source is a 46MB YouTube re-encode at ~800 kbps for 1146×480 — a heavily-compressed input. Sprite Fright at 105MB / 1920×804 succeeded. Hypothesis: Bitmovin AI needs a minimum visual fidelity (bitrate × resolution) to extract features.

Severity: P1: - Implicit billing surface: customers run encodings expecting just transcoding, but AI Scene Analysis runs and consumes additional quota / budget without being asked. - Implicit success/failure: no template config means the customer can't tell whether AI ran, succeeded, or failed.

Fix (Product): 1. Make implicit-run an explicit account-level toggle, with a CLI/dashboard "AI Scene Analysis: ON for all VOD encodings" setting that customers can review. 2. Per-encoding override in the template — start.aiSceneAnalysis: { enabled: false } to opt out. 3. Failure visibility: when AI fails on a specific title, log a clear AI scene analysis failed: <reason> message in encoding.status().messages. Currently the latest runs have NO AI-related messages at all (visibility regression — see item #26).

Fix (Docs): 1. Document the implicit-run behavior on the Encoding Templates page. 2. Document the input requirements: minimum bitrate, supported codecs, color spaces, etc. "AI scene analysis failed" with no detail isn't actionable.

26. AI status messages disappeared from encoding.status().messages — visibility regression

Earlier runs (a few hours before, with aiSceneAnalysis declared in the template):

- AI scene analysis in progress.
- AI scene analysis finished.   (or "AI scene analysis failed.")

Latest runs (without aiSceneAnalysis in the template, AI ran implicitly):

- Per-Title analysis finished.
- Encoding has finished successfully.
- Encoding process finished.
- Per-Title Encoding configuration finished.
- ... (zero AI-related messages)

The implicit AI run produces output (scenes.json lands in S3) but emits no status messages. So a customer using api.encoding.encodings.status(...).messages to monitor cannot tell: - whether AI ran at all - whether AI succeeded - whether AI failed and why

Severity: P1 — observability hole that makes debugging Spring-style failures impossible.

Fix (Encoding service): Always log AI lifecycle events (AI_STARTED, AI_FINISHED, AI_FAILED: <reason>) regardless of whether AI was explicit-config or implicit-account-default.

27. Encoding messages list returns events in non-chronological order — confirmed across multiple encodings

Sample from Spring's encoding:

- Per-Title analysis finished.        ← but also...
- Per-Title Encoding configuration started.
- Download of input file finished
- Download of input file in progress.   ← "in progress" AFTER "finished"
- Per-Title analysis setup started.
- The following HLS Manifests were removed due to Stream Conditions: ...
- Per-Title analysis started.          ← "started" AFTER "finished"
- Analyzing input in progress.
- Analyzing input finished.
- Per-Title Encoding configuration finished.
- Encoding has finished successfully.
- Encoding process finished.
- Per-Title analysis setup finished.
- The following DASH Manifests were removed due to Stream Conditions: ...
- Encoding in progress.                ← terminal-state event before "finished"

Already filed as item #14 above. This pattern is systematic across every encoding I inspected — the messages array is essentially a randomly-ordered set, not a log.

Fix (API): Return messages sorted ascending by date (which they have but aren't sorted by) or add an explicit sequence field.

28. Stream Lab dashboard URL pattern + suite reference — for documentation

Useful for the docs team: when an SDK consumer creates a stream via POST /v1/player/testing/streams, they get back an id and a suiteId. To find the stream in the dashboard:

https://dashboard.bitmovin.com/player/stream-lab/streams/{id}
or
https://dashboard.bitmovin.com/player/stream-lab/suites/{suiteId}/streams

(Verify exact URLs, but the SDK submit_stream() should print one of these so users know where their stream went.)

Fix (SDK): When a stream is created, log a clickable dashboard URL: Created stream {id} → https://dashboard.bitmovin.com/player/stream-lab/streams/{id}.


Updated TL;DR — after three sessions

The product is good. The polish is uneven. Top fixes ranked by adoption impact:

  1. POST /v1/player/testing/runs to actually trigger Stream Lab tests — this is the difference between Stream Lab being a real product or a dashboard-only feature. (Item #24, #19, #8)
  2. playerCfg.ui should reject true at construction — silent UI failures are unforgivable. (Item #17)
  3. POST /encoding/templates/validate dry-run endpoint — kills 90% of iteration cycles. (Item original-1)
  4. Multi-license analytics dashboard default to "All licenses" + is_default flag in SDK — eliminates the "no data showing" support tickets. (Item #18)
  5. Make AI Scene Analysis implicit-run explicit — opt-in/opt-out toggle, lifecycle messages, input-quality requirements documented. (Items #25, #26, original-6)
  6. Structured-error responses with path + expected + suggestion — makes agents self-heal. (Item original-2)
  7. One canonical YAML cookbook for the 10 most common workflows — turns hours into minutes. (Item original-D)
  8. Default-manifest path constraint + sort messages chronologically — small fixes, high cumulative impact. (Items original-16, #27)

Meta-observation, refined

Of 28 friction items, none are missing-feature gaps in what Bitmovin's product range can do — Per-Title encoding, AI Scene Analysis, Player + Analytics, Stream Lab device matrix all genuinely deliver world-class output when they work. The friction is overwhelmingly in: - Surface area: misnamed endpoints (/streams doesn't run anything), overloaded fields (ui: true), implicit behavior (account-default AI runs), missing structured errors. - Doc/SDK lag: the JSON schema lives in a different repo than the docs; the public docs page truncates with ...; SDKs lag the API by months for new features (no AI Scene Analysis SDK accessor in 1.264). - Dashboard ↔ API divergence: the dashboard does things (run a Stream Lab test, switch license views, group impressions across licenses) that the API can't.

For AI coding agents as a customer segment: silent-success failure modes (Items #17, #20, #25) and dashboard-vs-API mismatches (Items #18, #19, #24) are the biggest blockers. An agent's E2E test passing doesn't prove the product works; only humans can currently catch these gaps. Bitmovin should aim for: "if the agent's automated tests pass, the user-visible product works." That's the bar competitors will set, fast.

— Updated 2026-04-26, third session (after Stream Lab API + AI investigation).


Update — fourth session: real Stream Lab geo run + Lambda Function URL gotcha (2026-04-26)

After ran a real 20-country Stream Lab test using the targets API I'd missed, plus built an "▶ Start 10-min Live" button on the website.

29. Lambda Function URL with authorization_type = "NONE" returns persistent 403 even with the right resource policy

I tried first to expose the live-start Lambda via aws_lambda_function_url with authorization_type = "NONE". The function URL returned 403 Forbidden / x-amzn-ErrorType: AccessDeniedException for every request, despite:

{
  "Sid": "AllowPublicInvoke",
  "Effect": "Allow",
  "Principal": "*",
  "Action": "lambda:InvokeFunctionUrl",
  "Resource": "arn:aws:lambda:us-east-1:710922701336:function:bitflix-live-start",
  "Condition": { "StringEquals": { "lambda:FunctionUrlAuthType": "NONE" } }
}

Direct aws lambda invoke worked (Lambda code ran). The 403 was at the URL gateway layer.

I waited >5 minutes for IAM propagation; didn't help. I removed and re-added the policy; didn't help. Switched to API Gateway HTTP API instead — worked first try.

Without a clear error from Lambda's URL gateway (no log entry, no CloudTrail event explaining the deny), I never figured out why this is blocked on this account. This may be an org-level SCP, an account-level Lambda config, or a regional issue.

Severity: P1 for serverless adoption. Function URL is supposed to be the easy path for browser-callable Lambda.

Fix (AWS, not Bitmovin): when authorization_type = "NONE" is set but a request is denied, the response should include the policy reason in the body or at least a CloudTrail event. Today there's nothing actionable.

30. API Gateway HTTP API has a hard 30-sec timeout — Lambda has 15-min

Once on API Gateway, my first naive design had live-start doing the full provisioning (create encoding → create streams/muxings → start live → poll for RTMP IP, ~3 min total). API Gateway returned 503 "Service Unavailable" at 30 seconds; Lambda kept running invisibly to the user.

The fix: split into a fast Lambda (returns under 30s with the predicted live URL) that async-invokes a slow Lambda (15-min runtime). The frontend shows a 90-sec "ready countdown" while the slow Lambda finishes provisioning + starts ffmpeg.

This is a well-known AWS pattern but the first failure mode is silent + asymmetric (frontend gets 503, Lambda finished a successful run 2 min later). Documenting this gotcha in any "browser-callable Bitmovin Live trigger" guide would save hours.

Fix (Bitmovin): the live-encoder provisioning sequence should be fast enough that a single sync API call from API Gateway is feasible. Today, encodings.live.start() returns quickly but encodings.live.get(encoding_id) returns errorCode 2023 "Live encoding details not available" for 60-180s afterward. If the SDK provided a single wait_for_rtmp_ready(timeout=300) helper that handles this dance internally, it would make Bitmovin Live Encoder much simpler to integrate behind any HTTP-triggered serverless function.

31. Stream Lab "VPN locations" simulate egress only — all tests run from one cluster

Ran 20 country tests, expected per-country test runners. Every single result has summary.location: eu:klu:b2 (Klagenfurt, Austria — Bitmovin HQ). The "VPN location" only routes outbound network traffic through that country; the test runner itself stays in Klagenfurt.

Why this matters for users: 1. Startup times are NEARLY IDENTICAL across all 20 countries (range: 2195-2333 ms, median 2241 ms, spread 140 ms). For 20 geographically-separated runs you'd expect 200-500 ms variance from CDN edge propagation alone. We're not seeing that because the test runner is in one place, only the egress is varied. 2. The product page implies "test from real device, real location" — the implementation is "test from one location, route through a country's VPN." These mean different things for a CDN-performance demo. 3. For geo-blocking detection the current VPN model is correct. For geo-performance characterization it isn't.

Fix (Bitmovin): clarify the docs. Either: 1. Rename vpnLocationIdegressLocationId to make the limitation explicit. 2. Add real per-country test runners (massively more expensive, but actually delivers what the product implies). 3. Add a region query/filter to choose between known runner clusters when more become available.

32. Stream Lab job status notified is a terminal state but undocumented

My polling code matched job statuses finished, done, completed, success, failed, error, canceled, cancelled — the standard set for any async API. Stream Lab uses notified as the terminal "we sent you results" status. My poller hung forever waiting for one of the canonical statuses.

Fix (Docs): list the full status enum on the API docs page, with a clear note on which states are terminal. Today the only way to find notified is to inspect a real response.

33. Stream Lab test results don't include explicit "startup time" or "average bitrate" fields

I expected something like result.startupTime, result.averageBitrate, result.errorCount. The actual response shape:

{
  "items": [
    {"name": "Playback start", "result": "success", "durationMs": 2314, ...},
    {"name": "Validate stream playback", "durationMs": 2354, ...},
    {"name": "Play all renditions", "durationMs": 92274, ...},
    {"name": "Seek into middle while playing", "durationMs": 2336, ...},
    ...
  ]
}

Each "test" has name + durationMs. To extract startup time, you have to know "Playback start" is the name to filter on. To get bitrate you'd need a different field that isn't surfaced at all (Play all renditions is the closest).

Fix (API): add summary.metrics: { startupTimeMs, avgBitrateBps, droppedFrames, segmentLoadP95 } on the job response. Fix (Docs): list every test name + its meaning. Today the names are hardcoded magic strings.

34. Stream Lab tests fire real Bitmovin Analytics impressions

Each Stream Lab job has impressionId per test in the results. Looking at my "Stream Lab" license dashboard would have shown 20 × ~5 impressions = 100+ events (one per test that loads the player). I'd been confused earlier (item #18) about the "Stream Lab license" being empty — it's empty because we ran tests via the default-license, not the Stream Lab license. Discovery cost: the test results show impressionId but the license used per test is not exposed in the API.

Fix (API): include analyticsLicenseKey in the job response so callers can correlate Stream Lab runs with Analytics dashboard data.

35. No way to specify which Analytics license a Stream Lab job uses

Related to #18 + #34: even if I wanted Stream Lab tests to land in the "Stream Lab" license dashboard view (the pre-named license on Stefan's account), there's no parameter to choose. Stream Lab implicitly uses the account's "default" Analytics license.

Fix: POST /streams/<id>/targets should accept analyticsLicenseKey to override per-stream-target.


Updated success summary (after this session)

What worked first try and is genuinely impressive:

If Bitmovin closes the polish gaps in this document (explicit error messages, structured paths, missing SDK accessors, clearer docs about what a feature really does), the platform becomes spectacular.


Geographic Stream Lab summary (live, this session)

country                       startup_ms  manifest_ms  load_ms  rungs_ms
Australia - Sydney                  2256            4      152       524
Brazil                              2284            7      147       638
Canada - Toronto                    2239            3      126       490
France                              2202            3      103       413
Germany Frankfurt                   2267            5      202       601
India                               2244            3      125       486
Israel                              2206            3      100       464
Italy - Naples                      2234            3      137       490
Japan                               2333           10      143       566
Mexico                              2196            4       99       467
Netherlands - Amsterdam             2202           38      143       416
Poland                              2269            3      129       554
Singapore - CBD                     2286            5      139       499
South Africa                        2195            2       91       462
Spain                               2294            4      173       648
Sweden                              2203            2       96       464
Turkey                              2231            3      161       538
UK - London                         2296            4      258       536
USA - Los Angeles                   2209            3       98       410
USA - New York                      2314            6      322       548

— All from cluster eu:klu:b2 (Klagenfurt), Chrome 129 on Linux 22.4. Updated 2026-04-26, fourth session.


Update — fifth session: real-browser geo via browser-use Cloud (2026-04-26)

After the Stream Lab "VPN locations" finding (#31), I rebuilt the geo test using browser-use Cloud's wss://connect.browser-use.com?proxyCountryCode=<cc> Playwright CDP endpoint. Each country gets a real residential proxy + real Chromium session driven from my local script.

50 countries × Chrome on browser-use → bitflix.slederer.com → CloudFront. Concurrency 5, ~5 min wall-clock, ~$2 in browser-time.

The data shows what Stream Lab's VPN test couldn't. Time-to-playing range:

fastest:  Sweden       3,758 ms   (155.4.92.186)
median:                6,730 ms
mean:                  7,334 ms
slowest:  New Zealand 13,672 ms   (122.58.188.216)

Vs. Stream Lab's range (eu:klu:b2 cluster only): 2,195 ms – 2,333 ms (140 ms spread).

Stream Lab compresses real-world variance by ~100× because the runner is in one place. browser-use Cloud's residential-proxy approach exposes meaningful per-country CDN behavior:

The slowest cluster (APAC/AF/IE) correlates strongly with my CloudFront PriceClass_100 config (NA+EU edges only). APAC traffic round-trips to a US/EU edge → +5-9s vs. a regional edge would deliver. Switching to PriceClass_All would likely halve the median for those countries (a Bitflix-side fix, not a Bitmovin one — but worth flagging in any "your CDN matters" demo content).

36. Stream Lab vs browser-use Cloud as competing geo-test approaches

This run also clarified something the marketing pages don't: Stream Lab's "VPN location" feature is fundamentally different in what it measures from a residential-proxy + real-browser test. They are not substitutes:

Stream Lab vpnLocationId browser-use Cloud + proxyCountryCode
Test runner location One cluster (currently eu:klu:b2) One cluster (browser-use's region)
Egress IP VPN data-center in target country Residential proxy in target country
Median startup variance across 20-50 countries 140 ms ~10,000 ms
Cost per country test 1 of 20 monthly executions ($) ~$0.04 in browser-time
Can be CI-gated? Yes (after items #19, #24 retraction) Yes (off-the-shelf)
Captures CDN edge selection? No (single edge cluster) Yes (per-country residential ISP routing)
Captures DRM device matrix? Yes (31 real device configs incl. PS5/Xbox/Smart TVs) No (just Chromium)

Each tool is best for what it does. Stream Lab is unmatched for device-matrix QA (Smart TV / console / multi-OS validation). browser-use Cloud is far better for per-country CDN performance benchmarking.

A Bitmovin product gap worth considering: a "real per-country test runner" tier (or partner integration) would close the gap on the dimension Stream Lab currently cannot cover. Today that role is filled by the SaaS competitors (Catchpoint, Calibre, Pingdom, ThousandEyes) at much higher price points. Bitmovin Stream Lab + per-country runners would be a strong differentiator vs. JW Player's stack.

37. browser-use Cloud uses uk not gb for the United Kingdom

Submitting proxyCountryCode=gb returns:

HTTP 422 {"detail":[{"type":"enum","loc":["body","proxyCountryCode"],"msg":"Input should be 'ad', ..."}]}

ISO 3166-1 alpha-2 says GB. browser-use uses UK. Fine if documented; the docs say "195+ countries supported" without listing the exact codes. Fix (browser-use): accept both, or list the supported codes explicitly.

— Geographic data added 2026-04-26, fifth session.


Update — sixth session: Bitmovin's already-shipped agent surface (2026-04-27)

The CEO sent me to three resources I'd missed: - https://github.com/bitmovin/cli — the official CLI (@bitmovin/cli, npm install -g) - https://developer.bitmovin.com/llms.txt — LLM-friendly docs sitemap - https://agentic.bitmovin.com/{documentation,encoding,player}/mcp — three MCP servers ready to plug into Claude Code / Claude Desktop / ChatGPT / Cursor

I had not encountered any of them during the first 5 sessions of this build. That is itself the most important data point in this whole document.

38. Bitmovin's agent-friendly tooling already exists. Almost no agent will discover it.

Concrete reproduction. I configured the docs MCP via claude mcp add bitmovin-docs --transport http https://agentic.bitmovin.com/documentation/mcp and asked it the 5 highest-impact friction questions from this entire document:

Friction item ask_bitmovin_docs answer
#2 (Encoding Template codec ref syntax — $/path?) ❌ Wrong — said use codecConfigId: <bare-id>, which is exactly the trap I fell into
#3 (Does {number} in segmentNaming substitute?) ❌ Speculative — "highly probable that... substitute"; reality is the opposite
#5 (S3 IAM policy needing GetBucketLocation) ✅ Correct — full canonical IAM JSON returned
#19/#24 (Stream Lab trigger sequence) ⚠️ Deferred to docs page rather than answering
#25 (AI Scene Analysis implicit-by-account?) ⚠️ Said "you must enable per-encoding"; reality is the opposite (account default ran AI on 9/10 of my titles without me asking)

Net: 1/5 fully right, 1/5 partial, 3/5 wrong/non-answer. That 1 right answer would still have saved me ~15 min on this build (item #5).

The MCPs are only as good as the underlying docs. They're a real accelerator on the parts of the docs that are complete and accurate — but they don't fix the structural gaps (missing schema URLs, undocumented enum values, dashboard ↔ API divergence, etc.) called out everywhere else in this file. The MCPs are a multiplier on docs quality, not a substitute for it.

So fixing discoverability + the docs themselves are both required.


Recommendations: where to place the agentic surface so AI agents actually find it

Ranked by impact on time-to-first-success for a coding agent encountering Bitmovin for the first time. Each fix is < 1 day of work.

Tier 1 — surface the existing assets where agents look

  1. Promote llms.txt from the developer portal homepage. Add a <link rel="llms_txt" href="/llms.txt"> and a small banner in the hero section: "Building with an AI coding agent? Start here →". Then reference it in the API quickstart, the API key creation modal, and the Encoding/Player/Analytics homepages. Today no Bitmovin doc page mentions it.

  2. Add the MCPs to llms.txt as actual MCP endpoints, not just doc pages. Today llms.txt lists "Documentation MCP" and "Encoding MCP" as .md doc URLs. An agent reading llms.txt can't distinguish "this is the actual endpoint" from "this is a page about the endpoint." Format that section like:

``` ## MCP Servers (use these from any LLM client) - sse://agentic.bitmovin.com/documentation/mcp (no auth — public docs Q&A + SDK example search) - sse://agentic.bitmovin.com/encoding/mcp (header: x-api-key — list/inspect encodings, explain failures) - sse://agentic.bitmovin.com/player/mcp (header: x-api-key — render Player in chat clients)

## CLI - @bitmovin/cli (npm install -g) — terminal interface mirroring the Encoding API ```

  1. In every API error response, end the developerMessage with a one-line MCP hint. Specifically for 4xx/5xx encoding errors:

developerMessage: "An unexpected error occurred. For automated diagnosis, attach the Encoding MCP at https://agentic.bitmovin.com/encoding/mcp and ask: explain why encoding <id> failed. RequestId: …"

Today an agent gets only the requestId. With the MCP hint, the next iteration self-heals.

  1. Add an MCP install snippet to the API key creation page (Bitmovin Dashboard → API Keys). When a user creates a key, the success modal should include:

``` For Claude Code: claude mcp add bitmovin-encoding --transport http \ https://agentic.bitmovin.com/encoding/mcp \ --header "x-api-key: "

For Claude Desktop / Cursor: copy this JSON into your config: ```

This single placement converts agent users on first contact, not after they've burned hours on docs.

  1. Surface the CLI in the same place. npm install -g @bitmovin/cli && bitmovin config set api-key <key> — one line under the MCP block.

Tier 2 — make the existing surface more discoverable from search engines

  1. Each MCP doc page should have a clean canonical URL with the connection snippet at the top of the page (above the explainer text, not below). Today https://developer.bitmovin.com/streams/docs/mcp-encoding opens with prose; the actual claude mcp add ... snippet is buried.

  2. Add an agentic-quickstart.md at the top of the docs site, like a one-pager that an LLM agent (or a developer's pasted-into-Claude-of-the-week) can read first. Should fit in 200 lines and answer: "I'm an AI coding agent and a user has just told me to build something on Bitmovin. What do I do in the first 30 seconds?"

  3. Mention llms.txt and the MCPs in the bitmovin-api-sdk-* README files on GitHub. Each SDK README is the first thing an agent reads when grep'ing for examples; a ## For AI Coding Agents section at the top would be high-leverage.

Tier 3 — content gaps to fill BEFORE the agentic surface can do its job

The MCPs are read-only over the docs. If the docs don't say it, the MCP can't say it. From the 5-question test above:

  1. Document $/path cross-reference syntax explicitly on the Encoding Templates docs page. Today the docs hint at it; the MCP got it wrong because the prose was ambiguous. Item #2.

  2. Document literal-vs-substituted placeholders ({number}, {height}, {bitrate}) in segmentNaming, spriteName, outputPath. Item #3.

  3. Document the AI Scene Analysis account-level default behavior. Either change the default to off (and make the implicit-on opt-in), or document loudly that AI runs on every encoding when enabled at the account. Items #25, #26.

  4. Document the Stream Lab device-matrix trigger sequence with a copy-pasteable cURL block on the Stream Lab API docs page. Today it's spread across 4 endpoints; an agent will not stitch them. Items #19, #24, #32.

  5. Publish machine-readable schemas at canonical URLs on developer.bitmovin.com:

    • developer.bitmovin.com/schemas/encoding-template.json (currently in bitmovin-api-sdk-examples)
    • developer.bitmovin.com/schemas/encoding-api.openapi.yaml
    • developer.bitmovin.com/schemas/player-config.d.ts

    Then list them in llms.txt. Once these exist, an agent can validate template YAMLs without speculation.

Tier 4 — add the missing tools to the agentic surface

  1. bitmovin-streamlab MCP with run_test_for_url(url, devices, vpn_locations) and get_results(job_id) tools. Today there's no Stream Lab MCP at all. Item #19.

  2. bitmovin-live MCP with start_live(stream_key), wait_for_rtmp_ready(encoding_id, timeout), stop_live(encoding_id). The 60-180s live.get provisioning gap (item #30) is the kind of thing an MCP can wait through behind a single tool call.

  3. Documentation MCP should include a validate_encoding_template(yaml) tool that runs the schema check server-side. Today there's no /templates/validate endpoint at all (item original-1); making it MCP-callable would be even more powerful.

Tier 5 — llms.txt content fixes (independent of placement)

  1. Add a ## Common Pitfalls section to llms.txt with one line per item from "what we wish we knew on day 1":

    ```

    Common Pitfalls

    • Encoding Template references use $/path, not bare IDs
    • segmentNaming "{number}" is treated as a literal — use defaults
    • playerCfg.ui valid values: false | UIConfig | undefined — never true
    • bitmovin-api-sdk-python ≤ 1.234 pins requests<2.25 — use ≥ 1.260 with Python ≥ 3.10
    • AI Scene Analysis needs ≥ 4 min content + ≥ 1 Mbps source bitrate
    • Live encoding RTMP IP takes 60-180s to surface after live.start()
    • s3:GetBucketLocation must be in your IAM policy (not just GetObject)
    • Stream Lab "VPN locations" simulate egress only; runner stays in eu:klu:b2
    • Multi-license analytics dashboard defaults to one license — switch to "All licenses" ```

    These are the 9 highest-leverage items from the 37 in this document. Putting them in llms.txt means every agentic client picks them up automatically.


Bottom line for the team

You've already shipped the agentic infrastructure. Three MCPs, an LLM-friendly docs sitemap, an official CLI. That's ahead of basically every video infra competitor.

The reason none of it helped me on this build is placement. Two specific changes that I'd ship in the next sprint:

  1. A 4-line agentic-quickstart block on the developer portal homepage and on the API key creation page, with claude mcp add ... + npm install -g @bitmovin/cli snippets. Discoverability fix.
  2. A new ## MCP Servers section in llms.txt listing actual endpoint URLs (not just doc pages). Cross-client coverage fix.

These two changes would have prevented at least 8-12 of the 38 items in this document. They're 1-day work each.

— Updated 2026-04-27, sixth session (after MCP investigation).


Update — seventh session: Player X + AISA-driven preview clips (2026-04-29)

Built per-title hover previews on the catalog page. Two-part task: (a) generate a 10-second clip for each of the 10 titles using Bitmovin Encoding's TimeBasedTrimmingInputStream driven by AISA's scene boundaries (no ffmpeg), (b) play those clips with Bitmovin Player X 10.1.5 via the v8-compat bundle. Hit four real friction items en route — every one documented below.

39. Player X (10.x) via the v8-compat bundle silently fails on progressive MP4 sources

I built a 10-second progressive MP4 per title and called:

player.load({ progressive: 'https://.../preview.mp4' })

Player X created a <video> element, never loaded any data, and emitted this in the page console:

[error] Uncaught framework error: FrameworkError: HLS Manifest parsing failed: missing #EXTM3U tag
        at https://cdn.bitmovin.com/player/web_x/10/bundles/playerx-bitmovin-v8.js:10:305078

So the bundle accepts the progressive field name (no API-shape error), then routes it through the HLS parser, which obviously fails. The page behavior: black box on hover, no visible error to the end user.

The fix on my side: add an HLS manifest output to the same encoding (Fmp4Muxings + manifests.hls.default.create) and call player.load({ hls: '.../preview/master.m3u8' }). That worked first try.

The official Player X v8-compatibility docs page https://developer.bitmovin.com/playback/docs/player-web-x-v8-compatibility says: "Player Web X is not yet feature complete, with some APIs being simple NOPs until PWX has integrated the feature." But:

Severity: P0 for any team migrating from v8 → Player X who happens to use progressive MP4 sources for thumbnails / ads / promos / shorts.

Fix (Player team): 1. Either implement progressive playback in Player X 10.x. 2. Or, until then, make player.load({ progressive: … }) reject synchronously with a clear message: "Progressive sources are not yet supported in Player X. Use HLS or DASH." 3. Add a "Source types: HLS ✅ / DASH ✅ / progressive ❌ (planned)" matrix to the v8-compatibility docs page.

40. The Player X v8-compat bundle's UI factory is a non-functional stub but the docs imply otherwise

When my playerCfg.ui was false (intentional — no UI for thumbnail previews), the bundle still emitted:

[warning] Could not load UI TypeError: (intermediate value)(intermediate value)(intermediate value).buildDefaultUI is not a function
        at https://cdn.bitmovin.com/player/web_x/10/bundles/playerx-bitmovin-v8.js:10:482591

buildDefaultUI is not a function — Player X's UI module isn't wired up in this bundle. Functionally not blocking (the player's video element still mounts and plays), but every page load logs a warning to the user's console, which trips automated test runners and looks bad in production.

Fix: when ui: false is passed, the bundle should skip UI loading entirely (don't even reach buildDefaultUI). The current code path appears to call the function unconditionally and only check the ui config later.

41. ask_bitmovin_docs MCP fabricated an AI Scene Analysis schema field that does not exist

I asked the docs MCP: "How do I configure Bitmovin AI Scene Analysis to generate a short highlight clip for an asset? What is the YAML template field?"

The MCP confidently returned a worked YAML example using:

aiSceneAnalysis:
  extractHighlightSegments: true
  highlightSegmentLength: 60

These fields do not exist in the Encoding Templates JSON schema (verified by parsing bitmovin-encoding-template.json directly). The actual aiSceneAnalysis.features block has only assetDescription, automaticAdPlacement, and outputLanguageCodes.

This is the second time in this build that the docs MCP returned a confident-but-wrong answer (the first was item #25's "you must enable per-encoding" claim, when in reality AISA runs implicitly at the account level). The pattern: when the docs are silent or ambiguous on a topic, the MCP fills the gap with plausible-looking but invented schemas.

Severity: P1 because hallucinated YAML is worse than a "I don't know" — it costs an iteration cycle to discover the field doesn't exist.

Fix: 1. Add an explicit "I don't know" fallback when the underlying retrieval doesn't return a schema-grounded match. Better to return "This isn't documented; check the JSON schema at developer.bitmovin.com/schemas/encoding-template.json" than a fabricated YAML. 2. Ship the JSON schema at a stable URL (item #13) and have the docs MCP cite it. 3. For schema-shaped questions ("what fields does X take?"), the MCP should consult the schema directly, not just the prose docs.

42. StartEncodingRequest.vod_hls_manifests takes ManifestResource, not VodHlsManifest (which doesn't exist)

After producing an HLS manifest with manifests.hls.default.create(...), I needed to reference it from StartEncodingRequest:

StartEncodingRequest(vod_hls_manifests=[VodHlsManifest(manifest_id=hls.id)])
# → ModuleNotFoundError: No module named 'bitmovin_api_sdk.models.vod_hls_manifest'

The correct class is ManifestResource. The field name vod_hls_manifests strongly implies a VodHlsManifest model class (what I tried), but the SDK exposes only the generic ManifestResource. Even worse, LiveHlsManifest and LiveDashManifest do exist as VOD-cousin classes, so the asymmetry is confusing.

Fix (SDK): alias VodHlsManifest = ManifestResource (and the dash sibling) so the obvious-named classes work. Or rename the field on StartEncodingRequest to vod_hls_manifest_resources to remove the implication.

Summary of session-7 findings

# Severity Item
39 P0 Player X silently fails on progressive MP4 sources
40 P1 Player X UI factory stub emits a warning even when ui: false
41 P1 Docs MCP fabricates schema fields that don't exist
42 P2 SDK naming: VodHlsManifest doesn't exist; use ManifestResource

— Updated 2026-04-29, seventh session.

43. Player X has a native API at a different URL — but the v8-compat docs don't tell you about it

After items #39 and #40, Stefan pointed me at:

I had been using https://cdn.bitmovin.com/player/web_x/10/bundles/playerx-bitmovin-v8.js — the v8 compatibility shim — for the entire build. None of the public docs I'd read pointed at the native API:

The native API is dramatically nicer for a "drop-in playback" use case:

// v8-compat (what I had):
const player = new bitmovin.player.Player(container, { key, playback: {...}, ui: false });
await player.load({ hls: url });        // ← chokes silently on { progressive: ... }
await player.play();
// → console warning every time: "buildDefaultUI is not a function"

// Native Player X (where I switched to):
const player = bitmovin.playerx.Player({ key, defaultContainer: container, playback: {...} });
const source = player.sources.add({ resources: [{ url }] });  // ← no progressive support
                                                              //   either, but cleaner shape
source.play();
// → zero console warnings

Switching the catalog cards to the native bundle dropped console-noise to zero, kept the hover startup time (~770-860ms), and made the code shorter.

Two specific issues with how the native API is currently surfaced:

  1. The /playback/docs/player-web-x-v8-compatibility page should open with: "This is the v8 compatibility layer for users migrating from v8. New integrations should start with the native API at https://cdn.bitmovin.com/player/web_x/beta/10/docs/index.html." Today there is zero hint that a native Player X API exists.

  2. The bundle-locator namespace itself is confusing: the v8-compat bundle exposes bitmovin.player.Player (capital P, factory'd via new), the native bundle exposes bitmovin.playerx.Player (lowercase x, factory function, no new). If both are present on the same page (because someone loads both during migration), the one-character difference is a perfect footgun.

Severity: P1. The native API is what the Player team obviously wants new builds to use; the docs path keeps customers in v8-compat by default.

Fix: 1. The Player Web X documentation hub at developer.bitmovin.com/playback/docs/ should have a top-level "Player Web X" landing page that points at the native bundle first, with v8-compat as a secondary "migration" path. 2. Add developer.bitmovin.com/playback/docs/player-web-x (the URL I tried first — it 404s today) as the canonical native-first docs page. 3. The v8-compat console banner (Using Bitmovin v8 Bundle) should include: "For new integrations, see the native Player X API at https://cdn.bitmovin.com/player/web_x/beta/10/docs/index.html". 4. Mention both bundles + the docs URL in llms.txt.

— Recommendation #43 added 2026-04-29, seventh session continued.

44. Player X 10.x beta playback + HLS bundles never attach the source to the <video> element

After switching the tile previews from the v8-compat shim to the native API (item #43), tile hover went silent. I dug in with the live page + bitmovin.playerx.Player({...}) directly. What I found is genuinely broken in beta-10:

// Native API, exactly as the docs example shows:
const player = bitmovin.playerx.Player({
  key: KEY,
  defaultContainer: wrap,                              // <div>
  packages: [bitmovin.playerx.HlsBundlePackage()],     // required call shape
  playback: { autoplay: true, muted: true },
});
player.sources.add({ resources: [{ url: hlsManifestUrl }] });

// Result, after 4 seconds:
// container has a <video> element, but:
//   v.src        = ""
//   v.networkState = 0   (NETWORK_EMPTY)
//   v.readyState   = 0   (HAVE_NOTHING)
//   v.duration     = NaN
// Console:
//   [INFO] Source (content) added [id: 19dd...]      <- source registered
//   ... and then nothing. No error, no SourceError, no PlayerError.

Reproduced under multiple combos:

Bundle Packages Playback config User gesture? Result
playerx-playback.js none { autoplay:false, muted:true } yes vSrc="", no events
playerx-playback.js [PlaybackBundlePackage()] { autoplay:false, muted:true } yes SourceAdded fires, then PlayerError (the playback-only bundle doesn't include any format support — but PlayerError.message was empty, no hint that you need a format package)
playerx-playback.js [PlaybackBundlePackage()] { autoplay:true, muted:true } yes autoplay error, no playback
playerx-hls.js none (docs-canonical) none yes vSrc="", no events
playerx-hls.js [HlsBundlePackage()] { autoplay:true, muted:true } yes (real click) vSrc="", no events, no errors

Specific issues:

  1. packages is required but not documented for the playback-only bundle. The docs MCP example shows Player({...}); player.sources.add(...) and nothing else. In reality, calling that with the playback-only bundle adds the source to a player that has zero format-handler packages registered — silently. The code path that processes the source has nothing to dispatch to, so <video>.src stays empty and no error fires. There's no console hint, no PlayerError event, no rejected promise.
  2. player.packages.add(pkg) throws Cannot read properties of undefined (reading 'every'). The signature is add([pkg]) (array), but the type error message gives no clue. And add runs synchronously and silently returns even when called with the right shape — but the ergonomic add(pkg) (single value) blows up with an internal property error rather than a type error.
  3. PlaybackBundlePackage is a function, not a class. new bitmovin.playerx.PlaybackBundlePackage() throws is not a constructor; you have to call it as bitmovin.playerx.PlaybackBundlePackage() to get the package instance. Foo() vs new Foo() is a coin flip with the current export style.
  4. Even with the canonical example exactly as written (HLS bundle, no packages, no config), sources.add() doesn't trigger network activity for a public HLS manifest. The video element is created, but nothing else happens. Compare to native <video src="...mp4" autoplay muted loop> which Just Works in the same page in the same browser.
  5. SourceError and PlayerError events with empty message/stack give the agent nowhere to go. When the playback-only bundle threw PlayerError, I caught the event, logged its keys (name, message, stack, timestamp, type) — and message was the empty string. With no error text, an LLM agent has to guess.

Net effect: I spent 90 minutes trying to make Player X 10.x beta play a tile preview, abandoned the effort, and shipped tile previews using a native <video> element with the AISA-derived MP4 (matching the hero, which already worked). The native fallback is sub-second and looks identical.

Severity: P0. The "30-second hello world" is the most important demo Bitmovin Player X has. Today, copying the beta-10 docs example into a real page and pointing it at a public HLS manifest does not produce a playing video.

Fix: 1. Add a smoke-test page at cdn.bitmovin.com/player/web_x/beta/10/demo/index.html that loads playerx-hls.js, calls Player({...}).sources.add({...}), and plays a CDN-hosted manifest. If that page doesn't play, the bundle is broken and CI should catch it. 2. Make sources.add reject (or fire SourceError with a real message) when no format-handler package is registered. Today the source goes into limbo and the symptom is invisible. 3. Document packages as required for the modular bundles. Player({key, defaultContainer, packages: [HlsBundlePackage()]}) is the actual minimal example, not the two-line snippet currently shown. 4. Document the array shape of player.packages.add(...) — and accept a single value as a convenience, or throw a clear TypeError: packages.add expects an array. 5. Use class semantics consistently: either new PlaybackBundlePackage() everywhere or factory-functions everywhere — Player({...}) vs new Player(...) should not silently both compile but only one work. 6. Empty PlayerError.message is unforgivable. Every error should carry at minimum a human-readable string.

— Recommendation #44 added 2026-04-29, seventh session continued.

45. Player X v8-compat silently fails to schedule the lone segment of a single-segment HLS playlist

After #44 sent me back to the v8-compat shim (/player/web_x/10/bundles/playerx-bitmovin-v8.js), tile previews still didn't play. Same symptom: SourceLoaded, Ready, Play all fire; player.isPlaying() === true; but <video>.readyState = 0, duration = NaN, currentTime = 0 forever. No error, no warning, no SourceError.

I encoded the AISA-derived 10-second preview clips as single-segment HLS to minimize hover startup latency:

#EXTM3U
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-TARGETDURATION:10
#EXT-X-MAP:URI="video/init.mp4"
#EXTINF:10.0,
video/segment_0.m4s
#EXT-X-ENDLIST

Player X v8-compat doesn't load this segment. It loads the master playlist, the variant playlist, and… stops. The single segment never gets requested. The MSE buffer never receives data. The <video> stays empty.

The same encoding template, just with segment_length=2 instead of segment_length=12 (so the 10-second clip becomes five 2-second segments), played instantly through Player X v8-compat: blob URL attached, MSE fed, currentTime advancing within ~50 ms.

Severity: P1. Single-segment HLS is a perfectly valid playlist shape — it's the minimum for a short clip and the natural result of Fmp4Muxing(segment_length=<clip_duration>). The bug is also wholly invisible: no error, no warning, just isPlaying() === true while nothing actually plays.

Fix: 1. The HLS scheduler in Player X v8-compat must request the lone segment when the playlist contains exactly one segment between EXT-X-MAP and EXT-X-ENDLIST. Likely an off-by-one in the "advance to next segment" loop that requires next > current rather than >=. 2. A SourceWarning (or at least a console.warn) when the HLS scheduler exits the load loop with zero segments fetched would have surfaced this immediately. 3. Document the minimum reasonable segment_length (or document that single-segment HLS is unsupported) on the encoding side — this is a place where the Encoding and Player teams need to agree.

— Recommendation #45 added 2026-04-29, seventh session continued.