diff --git a/.cspell/custom-words.txt b/.cspell/custom-words.txt index 4bef035a6..e669861e6 100644 --- a/.cspell/custom-words.txt +++ b/.cspell/custom-words.txt @@ -170,3 +170,13 @@ punycode userinfo examplecorp zapatillas +# rich media formats (video / 3D model source types) +glb +gltf +m3u8 +mp4 +mpd +mpegurl +usdz +vnd +webm diff --git a/docs/specification/catalog/index.md b/docs/specification/catalog/index.md index 2964757ac..4d417655b 100644 --- a/docs/specification/catalog/index.md +++ b/docs/specification/catalog/index.md @@ -132,8 +132,57 @@ as the first element. Platforms SHOULD treat the first element as featured. ### Media +A single logical media item on a Product or Variant: an image, a video, or a +3D model. `url` identifies the item's primary resource, and its interpretation +depends on `type`. `preview` is a still image the Platform can render before — +or instead of — loading that resource. + +`type` is an open string with three well-known values: `image`, `video`, and +`model_3d`. A Platform **MUST** ignore a media item whose `type` it does not +recognize. + +When present, `sources` contains the complete set of available renditions or +encodings. A Business that provides `sources` **SHOULD** include a source whose +`url` equals the media item's `url`, unless `url` is an externally hosted +video's watch page rather than a rendition. + +For an image, a self-hosted video, or a 3D model, a Platform loads the +renderable or loadable primary resource at `url`. For an externally hosted +video with `embed_url`, `url` identifies the watch page and `embed_url` +identifies third-party active player content. A Platform **MAY** decline to +embed that content under its security, privacy, or presentation policy and use +the watch-page `url` instead. For a thumbnail, a Platform can use `preview.url` +and fall back to `url` for an image, which is directly displayable. + {{ schema_fields('types/media', 'catalog') }} +#### Image + +{{ schema_fields('types/media_image', 'catalog') }} + +#### Video + +A self-hosted video's `url` identifies its directly playable default rendition. +An externally hosted video is distinguished by `embed_url`, which identifies +the third-party active player content; its `url` identifies the watch page, not +a playable rendition. + +A Platform that selects a rendition from `sources` **SHOULD** use the `width` +and `height` of that entry rather than those on the media item, unless the +entry omits them. + +For an externally hosted video, a Business **SHOULD** provide both +`preview.width` and `preview.height` with a ratio that matches the intended +player, unless it has no suitable preview. A Platform **SHOULD** reserve the +initial player layout using that ratio, unless its presentation policy supplies +another ratio. + +{{ schema_fields('types/media_video', 'catalog') }} + +#### 3D Model + +{{ schema_fields('types/media_model_3d', 'catalog') }} + ### Product Option {{ schema_fields('types/product_option', 'catalog') }} diff --git a/docs/specification/catalog/mcp.md b/docs/specification/catalog/mcp.md index d92fa4ea3..f401de45b 100644 --- a/docs/specification/catalog/mcp.md +++ b/docs/specification/catalog/mcp.md @@ -533,7 +533,79 @@ Maps to the [Catalog Lookup](lookup.md#get-product-get_product) capability. Retu { "type": "image", "url": "https://cdn.example.com/products/runner-pro-blue.jpg", - "alt_text": "Runner Pro in Blue" + "alt_text": "Runner Pro in Blue", + "width": 1600, + "height": 1600 + }, + { + "type": "video", + "url": "https://cdn.example.com/products/runner-pro-demo-720p.mp4", + "name": "Runner Pro flex demo", + "alt_text": "Runner Pro shoe flexing to show sole cushioning", + "duration": 24, + "preview": { + "url": "https://cdn.example.com/products/runner-pro-demo-poster.jpg", + "width": 1280, + "height": 720 + }, + "sources": [ + { + "url": "https://cdn.example.com/products/runner-pro-demo-720p.mp4", + "mime_type": "video/mp4", + "format": "mp4", + "width": 1280, + "height": 720, + "filesize": 4200000 + }, + { + "url": "https://cdn.example.com/products/runner-pro-demo-1080p.mp4", + "mime_type": "video/mp4", + "format": "mp4", + "width": 1920, + "height": 1080, + "filesize": 8480000 + }, + { + "url": "https://cdn.example.com/products/runner-pro-demo.m3u8", + "mime_type": "application/vnd.apple.mpegurl", + "format": "m3u8" + } + ] + }, + { + "type": "model_3d", + "url": "https://cdn.example.com/products/runner-pro.glb", + "alt_text": "Runner Pro 3D model", + "preview": { + "url": "https://cdn.example.com/products/runner-pro-3d-thumb.jpg" + }, + "sources": [ + { + "url": "https://cdn.example.com/products/runner-pro.glb", + "mime_type": "model/gltf-binary", + "format": "glb", + "filesize": 4200000 + }, + { + "url": "https://cdn.example.com/products/runner-pro.usdz", + "mime_type": "model/vnd.usdz+zip", + "format": "usdz", + "filesize": 5100000 + } + ] + }, + { + "type": "video", + "url": "https://videos.example.com/watch?v=abc123", + "embed_url": "https://videos.example.com/embed/abc123", + "name": "Runner Pro trail review", + "alt_text": "Reviewer running a trail in the Runner Pro", + "duration": 212, + "preview": { + "url": "https://cdn.example.com/products/runner-pro-review-poster.jpg", + "width": 1280, + "height": 720 + } } ], "options": [ diff --git a/docs/specification/catalog/rest.md b/docs/specification/catalog/rest.md index cd5b2ca31..05518372d 100644 --- a/docs/specification/catalog/rest.md +++ b/docs/specification/catalog/rest.md @@ -409,7 +409,79 @@ on option values and returns variants matching the selection. { "type": "image", "url": "https://cdn.example.com/products/runner-pro-blue.jpg", - "alt_text": "Runner Pro in Blue" + "alt_text": "Runner Pro in Blue", + "width": 1600, + "height": 1600 + }, + { + "type": "video", + "url": "https://cdn.example.com/products/runner-pro-demo-720p.mp4", + "name": "Runner Pro flex demo", + "alt_text": "Runner Pro shoe flexing to show sole cushioning", + "duration": 24, + "preview": { + "url": "https://cdn.example.com/products/runner-pro-demo-poster.jpg", + "width": 1280, + "height": 720 + }, + "sources": [ + { + "url": "https://cdn.example.com/products/runner-pro-demo-720p.mp4", + "mime_type": "video/mp4", + "format": "mp4", + "width": 1280, + "height": 720, + "filesize": 4200000 + }, + { + "url": "https://cdn.example.com/products/runner-pro-demo-1080p.mp4", + "mime_type": "video/mp4", + "format": "mp4", + "width": 1920, + "height": 1080, + "filesize": 8480000 + }, + { + "url": "https://cdn.example.com/products/runner-pro-demo.m3u8", + "mime_type": "application/vnd.apple.mpegurl", + "format": "m3u8" + } + ] + }, + { + "type": "model_3d", + "url": "https://cdn.example.com/products/runner-pro.glb", + "alt_text": "Runner Pro 3D model", + "preview": { + "url": "https://cdn.example.com/products/runner-pro-3d-thumb.jpg" + }, + "sources": [ + { + "url": "https://cdn.example.com/products/runner-pro.glb", + "mime_type": "model/gltf-binary", + "format": "glb", + "filesize": 4200000 + }, + { + "url": "https://cdn.example.com/products/runner-pro.usdz", + "mime_type": "model/vnd.usdz+zip", + "format": "usdz", + "filesize": 5100000 + } + ] + }, + { + "type": "video", + "url": "https://videos.example.com/watch?v=abc123", + "embed_url": "https://videos.example.com/embed/abc123", + "name": "Runner Pro trail review", + "alt_text": "Reviewer running a trail in the Runner Pro", + "duration": 212, + "preview": { + "url": "https://cdn.example.com/products/runner-pro-review-poster.jpg", + "width": 1280, + "height": 720 + } } ], "options": [ diff --git a/source/schemas/common/types/media.json b/source/schemas/common/types/media.json index 57e28cfbc..de0bb7150 100644 --- a/source/schemas/common/types/media.json +++ b/source/schemas/common/types/media.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://ucp.dev/schemas/common/types/media.json", "title": "Media", - "description": "Media item (image, video, etc.).", + "description": "A single logical media item: an image, video, or 3D model. `url` is the primary resource; when present, `sources` is the complete set of available renditions. When `sources` is present and `url` identifies a rendition, the Business SHOULD include that rendition.", "type": "object", "required": [ "type", @@ -11,12 +11,16 @@ "properties": { "type": { "type": "string", - "description": "Media type. Well-known values: `image`, `video`, `model_3d`." + "description": "Media type. The Platform MUST ignore unknown values. Well-known values: `image`, `video`, `model_3d`." }, "url": { "type": "string", "format": "uri", - "description": "URL to the media resource." + "description": "Primary/canonical URL for the item; interpretation depends on `type` (specialized per type)." + }, + "name": { + "type": "string", + "description": "Human-readable title or label for the media, distinct from `alt_text` (accessibility text). Often present for `video` and `model_3d`." }, "alt_text": { "type": "string", @@ -25,12 +29,65 @@ "width": { "type": "integer", "minimum": 1, - "description": "Width in pixels (for images/video)." + "description": "Pixel width of the media." }, "height": { "type": "integer", "minimum": 1, - "description": "Height in pixels (for images/video)." + "description": "Pixel height of the media." + }, + "preview": { + "type": "object", + "description": "Poster/thumbnail still the Platform renders before or instead of the primary resource. A Business SHOULD provide it for `video` and `model_3d` unless no suitable still exists, and MAY provide it for `image` as a low-resolution placeholder. For externally hosted video, `preview.width` and `preview.height` provide the layout aspect ratio.", + "required": [ + "url" + ], + "properties": { + "url": { + "type": "string", + "format": "uri", + "description": "URL of the preview image." + }, + "width": { + "type": "integer", + "minimum": 1, + "description": "Actual pixel width of the preview image. The ratio of `preview.width` to `preview.height` is used to reserve embedded-video player layout." + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "Actual pixel height of the preview image. The ratio of `preview.width` to `preview.height` is used to reserve embedded-video player layout." + } + } + } + }, + "allOf": [ + { + "if": { + "properties": { "type": { "const": "image" } }, + "required": ["type"] + }, + "then": { + "$ref": "media_image.json" + } + }, + { + "if": { + "properties": { "type": { "const": "video" } }, + "required": ["type"] + }, + "then": { + "$ref": "media_video.json" + } + }, + { + "if": { + "properties": { "type": { "const": "model_3d" } }, + "required": ["type"] + }, + "then": { + "$ref": "media_model_3d.json" + } } - } + ] } diff --git a/source/schemas/common/types/media_image.json b/source/schemas/common/types/media_image.json new file mode 100644 index 000000000..1153a468c --- /dev/null +++ b/source/schemas/common/types/media_image.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/common/types/media_image.json", + "title": "Media Image", + "description": "Type-specific properties for image media.", + "type": "object", + "properties": { + "url": { + "description": "A directly displayable image." + }, + "width": { + "description": "Authoritative pixel width of the image." + }, + "height": { + "description": "Authoritative pixel height of the image." + } + } +} diff --git a/source/schemas/common/types/media_model_3d.json b/source/schemas/common/types/media_model_3d.json new file mode 100644 index 000000000..2266dec68 --- /dev/null +++ b/source/schemas/common/types/media_model_3d.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/common/types/media_model_3d.json", + "title": "Media Model 3D", + "description": "Type-specific properties for 3D model media.", + "type": "object", + "properties": { + "url": { + "description": "The primary model file (e.g. GLB)." + }, + "sources": { + "type": "array", + "description": "The complete set of available model encodings/file formats (glTF/GLB/USDZ). When present, the Business SHOULD include a source whose `url` equals the media `url`.", + "items": { + "$ref": "media_source.json" + } + } + } +} diff --git a/source/schemas/common/types/media_source.json b/source/schemas/common/types/media_source.json new file mode 100644 index 000000000..f89c7b21d --- /dev/null +++ b/source/schemas/common/types/media_source.json @@ -0,0 +1,40 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/common/types/media_source.json", + "title": "Media Source", + "description": "A single encoded rendition of a parent Media item — one resolution/format of a video, or one file format of a 3D model. The Platform selects the best supported source.", + "type": "object", + "required": [ + "url" + ], + "properties": { + "url": { + "type": "string", + "format": "uri", + "description": "URL to this rendition's file or streaming manifest." + }, + "mime_type": { + "type": "string", + "description": "IANA media type of this source. A Platform MUST NOT reject the parent media item solely because this value is unknown, but MAY skip a source it cannot decode. Examples: `video/mp4`, `video/webm`, `application/vnd.apple.mpegurl` (HLS), `application/dash+xml` (DASH), `model/gltf-binary` (GLB), `model/gltf+json` (glTF), `model/vnd.usdz+zip` (USDZ)." + }, + "format": { + "type": "string", + "description": "Short format token for display/selection. A Platform MUST NOT reject the parent media item solely because this value is unknown, but MAY skip a source it cannot decode. Examples: `mp4`, `webm`, `m3u8`, `mpd`, `glb`, `gltf`, `usdz`." + }, + "width": { + "type": "integer", + "minimum": 1, + "description": "Pixel width of this rendition (video). Omitted for adaptive manifests and 3D models." + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "Pixel height of this rendition (video). Omitted for adaptive manifests and 3D models." + }, + "filesize": { + "type": "integer", + "minimum": 1, + "description": "File size in bytes. Lets the Platform decide whether to prefetch a large asset (e.g. a multi-megabyte 3D model). Omitted for adaptive-streaming manifests, whose total size is not fixed." + } + } +} diff --git a/source/schemas/common/types/media_video.json b/source/schemas/common/types/media_video.json new file mode 100644 index 000000000..0c0520139 --- /dev/null +++ b/source/schemas/common/types/media_video.json @@ -0,0 +1,35 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/common/types/media_video.json", + "title": "Media Video", + "description": "Type-specific properties for video media.", + "type": "object", + "properties": { + "url": { + "description": "The directly playable default rendition for a self-hosted video, or the watch-page URL for an externally hosted video with `embed_url`. When `sources` is present and `url` identifies a rendition, the Business SHOULD include a source whose `url` equals the media `url`; this does not apply when `url` is an external watch-page URL." + }, + "embed_url": { + "type": "string", + "format": "uri", + "description": "Player embed (iframe) URL for an externally hosted video, through which the Platform may play the video; absent for self-hosted video. A Platform MAY decline third-party active content under its security, privacy, or presentation policy and use the watch-page `url` instead." + }, + "duration": { + "type": "integer", + "minimum": 1, + "description": "Duration in seconds." + }, + "width": { + "description": "The video's display width in pixels, for layout and aspect ratio. A selected rendition's exact width is in that `sources[]` entry." + }, + "height": { + "description": "The video's display height in pixels, for layout and aspect ratio. A selected rendition's exact height is in that `sources[]` entry." + }, + "sources": { + "type": "array", + "description": "The complete set of available encoded renditions (MP4/WebM, HLS/DASH manifests). A Business that provides `sources` SHOULD include a source whose `url` equals the media `url` when `url` identifies a rendition; an external watch-page `url` does not identify a rendition.", + "items": { + "$ref": "media_source.json" + } + } + } +}