diff --git a/CHANGELOG.md b/CHANGELOG.md index ca266e76a..97cdddff2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +# [1.2.0-next.4](https://github.com/rdkcentral/firebolt-apis/compare/v1.2.0-next.3...v1.2.0-next.4) (2024-06-06) + + +### Features + +* Command and Control Intents ([#251](https://github.com/rdkcentral/firebolt-apis/issues/251)) ([c8f8dae](https://github.com/rdkcentral/firebolt-apis/commit/c8f8dae5a9a0f14a3815c04df5a55763823d4898)) + +# [1.2.0-next.3](https://github.com/rdkcentral/firebolt-apis/compare/v1.2.0-next.2...v1.2.0-next.3) (2024-06-06) + + +### Features + +* User Interest ([#170](https://github.com/rdkcentral/firebolt-apis/issues/170)) ([48a1094](https://github.com/rdkcentral/firebolt-apis/commit/48a1094aaab6418f09db662dbc81f090a34f32ed)) + # [1.2.0-next.2](https://github.com/rdkcentral/firebolt-apis/compare/v1.2.0-next.1...v1.2.0-next.2) (2024-04-08) diff --git a/package-lock.json b/package-lock.json index 5894c4548..2bb08c1a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,17 @@ { "name": "@firebolt-js/sdks", - "version": "1.2.0-next.2", + "version": "1.2.0-next.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@firebolt-js/sdks", - "version": "1.2.0-next.2", + "version": "1.2.0-next.4", "license": "Apache-2.0", "workspaces": [ "src/sdks/core", - "src/sdks/manage" + "src/sdks/manage", + "src/sdks/discovery" ], "bin": { "firebolt-version": "src/js/version.mjs" @@ -1065,17 +1066,21 @@ "node": ">=8" } }, + "node_modules/@firebolt-js/discovery-sdk": { + "resolved": "src/sdks/discovery", + "link": true + }, "node_modules/@firebolt-js/manage-sdk": { "resolved": "src/sdks/manage", "link": true }, "node_modules/@firebolt-js/openrpc": { - "version": "3.0.0-next.3", + "version": "3.0.0-next.4", "resolved": "git+ssh://git@github.com/rdkcentral/firebolt-openrpc.git#17b7a317a44f38c7ea44913b152ad1ba804bf025", "dev": true, "dependencies": { - "ajv": "^8.3.0", - "ajv-formats": "^2.1.0", + "ajv": "^8.12.0", + "ajv-formats": "^2.1.1", "array.prototype.groupby": "^1.1.0", "crocks": "^0.12.4", "deepmerge": "^4.2.2", @@ -16950,6 +16955,17 @@ }, "src/sdks/core": { "name": "@firebolt-js/sdk", + "version": "1.2.0-next.3", + "license": "Apache-2.0", + "devDependencies": { + "jest": "^28.1.0", + "jest-environment-jsdom": "^28.1.3", + "prettier": "^3.1.0", + "typescript": "^4.6.4" + } + }, + "src/sdks/discovery": { + "name": "@firebolt-js/discovery-sdk", "version": "1.2.0-next.2", "license": "Apache-2.0", "devDependencies": { @@ -16961,7 +16977,7 @@ }, "src/sdks/manage": { "name": "@firebolt-js/manage-sdk", - "version": "1.2.0-next.2", + "version": "1.2.0-next.3", "license": "Apache-2.0", "devDependencies": { "jest": "^28.1.0", diff --git a/package.json b/package.json index 7729aa446..fc2112ddc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@firebolt-js/sdks", - "version": "1.2.0-next.2", + "version": "1.2.0-next.4", "description": "The Firebolt JS SDK", "type": "module", "bin": { @@ -8,14 +8,15 @@ }, "workspaces": [ "src/sdks/core", - "src/sdks/manage" + "src/sdks/manage", + "src/sdks/discovery" ], "scripts": { "fs:setup": "npm run clean && mkdir -p dist", - "validate:each": "npx firebolt-openrpc validate --input src/openrpc --schemas node_modules/@firebolt-js/schemas/src/schemas --schemas src/schemas --transformations", - "validate:compiled": "npx firebolt-openrpc validate --input dist/firebolt-open-rpc.json && npm run validate --workspaces", + "validate:each": "npx firebolt-openrpc validate --input src/openrpc --schemas src/schemas --transformations", + "validate:compiled": "npx firebolt-openrpc validate --input dist/firebolt-open-rpc.json --pass-throughs && npm run validate --workspaces", "validate": "npm run validate:each && npm run validate:compiled ", - "compile": "npx firebolt-openrpc openrpc --input src --template src/template/openrpc/template.json --output ./dist/firebolt-open-rpc.json --schemas node_modules/@firebolt-js/schemas/src/schemas --schemas src/schemas", + "compile": "npx firebolt-openrpc openrpc --input src --template src/template/openrpc/template.json --output ./dist/firebolt-open-rpc.json --schemas src/schemas", "slice": "npm run slice --workspaces", "sdks": "npm run sdk --workspaces", "docs": "npm run docs --workspaces", diff --git a/requirements/images/specifications/intents/user-interest/media/image1.png b/requirements/images/specifications/intents/user-interest/media/image1.png new file mode 100644 index 000000000..b0171a9a4 Binary files /dev/null and b/requirements/images/specifications/intents/user-interest/media/image1.png differ diff --git a/requirements/images/specifications/intents/user-interest/media/image2.png b/requirements/images/specifications/intents/user-interest/media/image2.png new file mode 100644 index 000000000..94102a60f Binary files /dev/null and b/requirements/images/specifications/intents/user-interest/media/image2.png differ diff --git a/requirements/images/specifications/intents/user-interest/media/image3.png b/requirements/images/specifications/intents/user-interest/media/image3.png new file mode 100644 index 000000000..c7ddb6405 Binary files /dev/null and b/requirements/images/specifications/intents/user-interest/media/image3.png differ diff --git a/requirements/images/specifications/intents/user-interest/media/image4.png b/requirements/images/specifications/intents/user-interest/media/image4.png new file mode 100644 index 000000000..6a15fe0c6 Binary files /dev/null and b/requirements/images/specifications/intents/user-interest/media/image4.png differ diff --git a/requirements/specifications/discovery/user-interest.md b/requirements/specifications/discovery/user-interest.md new file mode 100644 index 000000000..f53e239ba --- /dev/null +++ b/requirements/specifications/discovery/user-interest.md @@ -0,0 +1,361 @@ +# User Interest + +Document Status: Candidate Specification + +See [Firebolt Requirements Governance](../../governance.md) for more info. + +| Contributor | Organization | +| -------------- | -------------- | +| Eugene Chung | Comcast | +| Tim Dibben | Sky | +| Mike Horwitz | Comcast | +| Jeremy LaCivita | Comcast | + +## 1. Overview + +In additional to traditional discovery APIs such as Watch History and +Watch Next, Firebolt provides a more abstract API that facilitates +impromptu content discovery connections between first-party Aggregated +Experiences and third-party Apps. + +The User Interest Capability enables Apps to provide meta-data on +content that the user has expressed an interest in to Aggregated +Experience Apps that have been given access to use this Capability. + +This allows for open ended design of Aggregated Experience App features +that present App-specific content to re-engage the user with the content +inside the originating App. + +While the functionality and UX is left to the Aggregated Experience App, +typically designed by each Firebolt Distributor, the Firebolt API +enables events to register user interest and pass entity meta-data: + +![Diagram Description automatically +generated](../../../requirements/images/specifications/intents/user-interest/media/image1.png) + +Which generally enables Aggregated Experiences to present that entity +meta-data in some way that leads to re-launching the original App at a +later point, using a `navigateTo` notification: + +![Diagram Description automatically +generated](../../../requirements/images/specifications/intents/user-interest/media/image2.png) + +This is just one example of what an Aggregated Experience App might do +with the User Interest API. + +Note that this API **SHOULD NOT** be used to implement Watch History or +Watch Next features. These concepts are much more fundamental to +Firebolt and have explicit APIs so that Firebolt Distributors can keep +track of which apps are using them separately. + +## 2. Table of Contents +- [1. Overview](#1-overview) +- [2. Table of Contents](#2-table-of-contents) +- [3. User Interest Flows](#3-user-interest-flows) + - [3.1. User Interest from an in-app UX](#31-user-interest-from-an-in-app-ux) + - [3.2. User Interest from a platform UX](#32-user-interest-from-a-platform-ux) + - [User Interest Errors](#user-interest-errors) + - [3.3. Upstream User Interest Intent](#33-upstream-user-interest-intent) + - [3.4. User Interest Bulk Updates](#34-user-interest-bulk-updates) +- [4. Core SDK APIs](#4-core-sdk-apis) + - [4.1. InterestType](#41-interesttype) + - [4.2. InterestReason](#42-interestreason) + - [4.3. Discovery.userInterest](#43-discoveryuserinterest) + - [4.4. Discovery Interest Provider](#44-discovery-interest-provider) + - [4.5. InterestIntent](#45-interestintent) +- [5. Discovery SDK APIs](#5-discovery-sdk-apis) + - [5.1. Interest Types](#51-interest-types) + - [5.2. Content.requestUserInterest](#52-contentrequestuserinterest) + - [5.3. Content.onUserInterest](#53-contentonuserinterest) + + +## 3. User Interest Flows +### 3.1. User Interest from an in-app UX + +Some Apps will have a built-in user interface for users to express +interest in content from the App. This could be a "Favorite" button, +an in-app "My List" button, etc. + +If the App wants to leverage any additional exposure from the device's +Aggregated Experience, it can wire up its own UI to the Firebolt User +Interest API, in addition to any in-app features that it's already +invoking. + +By calling the `Discovery.userInterest` method with the relevant entity +meta-data, the device's Aggregated Experience will be notified of the +user's interest in that entity: + +```typescript +Discovery.userInterest(type: InterestType, reason: InterestReason, entity: EntityDetails) +``` + +The `type` parameter denotes the directionality of the interest: + +- `interest` +- `disinterest` + +The `reason` parameter denotes why or how the user has expressed interest: + +| Reason | Description | +| ------ | ----------- | +| `playlist` | Interested in adding to a list | +| `reaction` | Interested in submitting a reaction, e.g. like or dislike | +| `recording` | Interest in scheduling a recording | +| `share` | Interest in sharing the content on social media | + +**NOTE**: We can remove some of these (not `playlist`) these are here for now to illustrate the purpose for the reason paramater. + +An app **MUST** `provide` the `xrn:firebolt:capability:discovery:interest` +capability in order to call `Discovery.userInterest`. + +When this method is called with a valid `EntityDetails`, the platform +**MUST** dispatch a `Content.onUserInterest` notification to all Apps +that have registered for it (typically Aggregated Experience Apps) with +information about the app, interest type, and the entity. + +The `Content.onUserInterest` event has a result type of `Interest`: + +| property | type | description | +|---------|------|-------------| +| appId | string | The id of the app that pushed the user interest. | +| type | `InterestType` | the type of interest. | +| reason | `InterestReason` | the reason for the interest | +| entity | `EntityDetails` | The entity the user expressed interest in. | + +An Aggregated Experience can register for the `Content.onUserInterest` +notification, and it will receive notifications when an `EntityDetails` is +returned from the active App after a `Discovery.userInterest` call is +fulfilled. + +An app **MUST** have permissions to `use` the +`xrn:firebolt:capability:discovery:interest` capability in order to +listen to the `Content.onUserInterest` notification. + +If the result is not a valid entity, i.e. does not match +the [EntityDetails](../entities/) schema, then no `Content.onUserInterest` +notification will be dispatched. + +The `Discovery.userInterest` method **SHOULD NOT** be used in place of more +specific Discovery methods, e.g. `Discovery.watchNext` or +`Discovery.watched`. These methods facilitate specific UX flows that may +have separate legal opt-outs for each user. + +The `Discovery.userInterest` method **SHOULD NOT** be called unless the user +is activating a UI element in the app, or in a second screen experience +that is communicating with the app, that implies interest of some kind. + +### 3.2. User Interest from a platform UX + +Firebolt platforms may provide a platform UX, e.g. voice or and RCU, to +express user interest in content from an active App. To facilitate this +Apps will need to be told about the user's expressed interest in their +content. + +First, the Aggregated Experience (or some app with this +capability) detects that the user is interested in something. In this +picture the interest is triggered by an RCU button, but how this occurs +is outside the scope of this document. When this happens, the Aggregated +Experience app calls `Content.requestUserInterest()`, which will trigger the +platform to identify the best [Provider Candidate](../openrpc-extensions/app-passthrough-apis.md#5-provider-candidates) +and call that app's `userInterest` method via the Provider RPC method: +`Discovery.onRequestUserInterest`. + +![](../../../requirements/images/specifications/intents/user-interest/media/image3.png) + +Next, the provider app receives and responds to the request with an +EntityDetails, which is returned as the result to the pending +`Content.requestUserInterest` method: + +![](../../../requirements/images/specifications/intents/user-interest/media/image4.png) + +Once an App's callback is invoked, that app will have `interestTimeout` +milliseconds to return a value or throw an error. Values returned after +that time **MUST** be ignored. The timeout value is stored in the +device's configuration manifest. + +To be notified when a user expresses interest in the currently displayed +content, an App **MUST** provide the +`xrn:firebolt:capability:discovery:interest` capability by enabling the +`Discovery.onRequestUserInterest` notification. + +If there is a valid entity to return, then the method registered by the +App **MUST** return the currently displayed entity meta-data. + +If there is no valid entity to return, then the method **MUST** throw an +exception. + +If the provider app returns a valid `EntityDetails` before the timeout, +then, the returned value **MUST** be used. + +If there is no app registered the an error **MUST** be returned. + +#### User Interest Errors +An app is expected return either a valid result or an appriate error. + +If neither happens before `interestTimeout` expires then the platform **MUST** return a Provider timed-out error: + +```json +{ + "id": 1, + "error": { + "code": -50400, + "message": "Provider timed-out", + "data": { + "capability": "xrn:firebolt:capability:discovery:interest", + } + } +} +``` + +If an app recieves a request for user interest when there is nothing appropriate to return, e.g. nothing selected or presented that maps to an Entity, then the app **SHOULD** return an error with a valid JSON-RPC error response, e.g.: + +```json +{ + "id": 1, + "error": { + "code": -40400, + "message": "No entities currently presented." + } +} +``` + +The platform API Gateway **MUST** append, or overwrite, the `data.capability` value to any errors returned by the app, e.g.: + +```json +{ + "id": 1, + "error": { + "code": -40400, + "message": "No entities currently presented.", + "data": { + "capability": "xrn:firebolt:capability:discovery:interest", + } + } +} +``` + +### 3.3. Upstream User Interest Intent +In some cases, e.g. a voice assistant, some upstream component will inform +the platform that the user is interested in whatever is currently being presented. + +To do this, the upstream system **MUST** send a `Interest` intent, which describes the type of and reason for the interest. + +```json +{ + "action": "interest", + "data": { + "type": "interest", + "reason": "playlist" + } +} +``` + +When a Firebolt platform receives this intent, it **SHOULD** initiate the platform's [user interest flow](#4-user-interest-from-a-platform-ux). + +### 3.4. User Interest Bulk Updates + +Sending bulk interest updates, e.g. Entities the user expressed interest +in on a different platform, is not supported. + +## 4. Core SDK APIs + +The following APIs are exposed by the Firebolt Core SDK as part of the +`Discovery` module. + +### 4.1. InterestType +This is an enum with the following values: + +- `"interest"` +- `"disinterest"` + +### 4.2. InterestReason +This is an enum with the following values: + +| Reason | Description | +| ------ | ----------- | +| `playlist` | Interested in adding to a list | +| `reaction` | Interested in submitting a reaction, e.g. like or dislike | +| `recording` | Interest in scheduling a recording | +| `share` | Interest in sharing the content on social media | + +### 4.3. Discovery.userInterest + +This is a push API that allows Apps to push entities that the user has +expressed interest in to the platform. + +To push an entity that the user is interested in pass an `EntityDetails` +object to the method: + +```typescript +Discovery.userInterest(type: InterestType, reason: InterestReason, entity: EntityDetails): Promise +``` + +### 4.4. Discovery Interest Provider +To respond to requests for the current entity, because the user has +expressed interest in some way that the platform manages, register a +provider: + +```typescript +interface IDiscoveryInterestProvider { + function userInterest(type: InterestType, reason: InterestReason): Promise +} + +Discovery.provide("xrn:firbolt:capability:discovery:interest", IDiscoveryInterestProvider) +``` + +### 4.5. InterestIntent + +An `InterestIntent` denotes that the user has expressed interest in the +currently displayed and/or selected content: + +```typescript +type InterestIntent { + action: "interest" + data: { + type: "interest" | "disinterest", + reason: "playlist" | "reaction" | "recording" + }, + context: { + source: "rcu" | "voice" + } +} +``` + +## 5. Discovery SDK APIs + +The following APIs are exposed by the Firebolt Discovery SDK as part of the +`Content` module. + +### 5.1. Interest Types +This type stores the various attributes of an Interest response or event: + +```typescript +type InterestType = "interest" | "disinterest" +type InterestReason = "playlist" | "reaction" | "recording" | "share" + +type Interest { + appId: string + entity: EntityDetails + type?: InterestType + reason?: InterestReason +} +``` + +### 5.2. Content.requestUserInterest +This method triggers the corresponding Discovery provider API for the +provider app. + +```typescript +Content.requestUserInterest(type: InterestType, reason: InterestReason): Promise +``` + +### 5.3. Content.onUserInterest + +This notification allows Aggregated Experience Apps to be informed when +a user expresses interest in some Content, and the content resolves to a +valid Entity from some App. + +`Content.listen('userInterest', Interest => void): Promise` + +The callback will be passed an `Interest` object with +the appId, type, reason, and information about the entity that the user expressed interest in. diff --git a/requirements/specifications/general/context-parameters.md b/requirements/specifications/general/context-parameters.md new file mode 100644 index 000000000..761a03fd7 --- /dev/null +++ b/requirements/specifications/general/context-parameters.md @@ -0,0 +1,303 @@ +# Context Parameters +Document Status: Working Draft + +See [Firebolt Requirements Governance](../../governance.md) for more info. + +| Contributor | Organization | +| -------------- | -------------- | +| Jeremy LaCivita | Comcast | +| Yuri Pasquali | Sky | + +## 1. Overview + +**TODO**: This doc is old and need to be refreshed + +Context Parameters are parameters on an RPC method that provide context +for the call via a set of primitive types (string, number, integer, +boolean). This allows for sharing the context parameters across property +getters, setters, and subscribers, as well as filtering which events to +listen for by context. + +An example of a property method with context could be: + +```javascript +// get a context-driven property (context: appId=hulu) +const huluShare = await Privacy.shareWatchHistory('hulu') +``` + +```javascript +// set a context-driven property (context: appId=hulu) +Privacy.shareWatchHistory('hulu', false) +``` + +In the example above, 'hulu' is the context parameter for both the +shareWatchHistory getter and setter. + +Context parameters can also be applied to property subscribers, as well +as other, non-property events: + +```javascript +// subscribe to a context-driven property +Privacy.shareWatchHistory('hulu', (value) => { + console.log('hulu value changed to: ' + value) +}) + +Privacy.listen('shareWatchHistoryChanged', 'hulu', (value) => { + console.log('hulu value changed to: ' + value) +}) +``` + +For subscribers and events, the context parameters may be omitted, in +which case, all events will be dispatched to the listener: + +```javascript +// subscribe to a context-driven property w/out any context (get all of them) + +Privacy.shareWatchHistory((appId, value) => { + console.log(`App '${appId}' value changed to ${value}`) +}) + +Privacy.listen('shareWatchHistoryChanged', (appId, value) => { + console.log(`App '${appId}' value changed to: ${value}`) +}) +``` + +Context Parameters **MUST** be of a primitive type, to avoid complex +object-tree filtering. + +This document describes an OpenRPC pattern and JavaScript code +generation for a Firebolt method template that uses Context Parameters. + +## 2. Table of Contesnts +- [1. Overview](#1-overview) +- [2. Table of Contesnts](#2-table-of-contesnts) +- [3. Context Parameters Use Cases](#3-context-parameters-use-cases) +- [4. Context Parameters API](#4-context-parameters-api) + - [4.1. JSON-RPC API](#41-json-rpc-api) + - [4.1.1. Setter RPC generation](#411-setter-rpc-generation) + - [4.1.2. onChanged RPC generation](#412-onchanged-rpc-generation) + - [4.1.3. Temporal Set onAvailable / Unavailable RPC generation](#413-temporal-set-onavailable--unavailable-rpc-generation) + - [4.1.4. Event RPC Decoration](#414-event-rpc-decoration) + - [4.2. JavaScript API](#42-javascript-api) + - [4.2.1. Event Listener Signatures](#421-event-listener-signatures) + +## 3. Context Parameters Use Cases + +How each parameter affects it's corresponding API is out of scope for +this document. See each API spec for details on what each context +parameter does. + +Setting a context parameter to null is still setting it to a value. If a +context parameter is passed to the SDK with either a value of null or +undefined, then it **MUST** be explicitly set in the RPC request to the +value null. This is to avoid additional method signature permutations +being required for the SDK. + +An effort should be made to sort the context parameters in order of most +usefulness, since not all languages support undefined. + +## 4. Context Parameters API + +The section describes the RPC and JavaScript APIs. + +### 4.1. JSON-RPC API + +Simple getters and event listeners don't need any parameters. + +If a method is tagged as either a property (any kind) or an event, then +**all** the parameters in the RPC definition **MUST** be context +parameters. + +To facilitate this, the listen parameter that all events currently have +will be removed from the source module and inserted into the generated +RPC by the firebolt-openrpc tooling. + +If any Context Parameters have a type other than: + +- `string` +- `boolean` +- `number` +- `integer` + +Then the RPC method **MUST NOT** pass validation. This is to ensure that +implementing context parameters is not overly complicated. + +#### 4.1.1. Setter RPC generation + +When generating the setter for a property method, all the Context +Parameters **MUST** be copied to the setter. The context parameters +**MUST** be before the value parameter, which itself **MUST** be last. + +#### 4.1.2. onChanged RPC generation + +When generating the onChanged notification for a property +method, all the Context Parameters **MUST** be copied to the event +parameters. The context parameters **MUST** be before the listen +parameter, which itself **MUST** be last. + +#### 4.1.3. Temporal Set onAvailable / Unavailable RPC generation + +Generated Temporal Set events will treat the entire parameter list from +the original temporal-set method as Context Parameters and copy them to +the event parameters. The context parameters **MUST** be before the +listen parameter, which itself **MUST** be last. + +#### 4.1.4. Event RPC Decoration + +All RPC methods tagged as event **MUST** have the listen parameter, of +type boolean, automatically added when generating the final RPC. This +parameter will be added at the end of the parameters list. + +All RPC methods tagged as event that have context parameters **MUST** +have the result schema wrapped in an object. The original result schema +**MUST** be moved to a property called data. Each of the context +parameters **MUST** be copied to a property called context. + +So the following RPC event: + +```json +{ + "name": "onContextualEvent", + "tags": [ + { + "name": "event" + } + ], + "params": [ + { + "name": "a", + "required": true, + "schema": { + "type": "boolean" + } + }, + { + "name": "b", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "c", + "required": true, + "schema": { + "type": "number" + } + } + ], + "result": { + "name": "result", + "schema": { + "type": "object", + "properties": { + "foo": { + "type": "boolean" + } + } + } + } +} +``` + +Would have its result transformed to: + +```json +{ + "name": "result", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "foo": { + "type": "boolean" + } + } + }, + "context": { + "type": "object", + "properties": { + "a": { + "type": "boolean" + }, + "b": { + "type": "string" + }, + "c": { + "type": "number" + } + }, + "required": [ + "a", "b", "c" + ] + } + } + } +} +``` + +### 4.2. JavaScript API + +TBD + +#### 4.2.1. Event Listener Signatures + +If any of the context parameters are optional, then a callback signature +must be generated for each left-to-right combination of the parameters. + +For example, the method: + +```json +{ + "name": "onFoo", + "tags": [ + { + "name": "event" + } + ], + "params": [ + { + "name": "a", + "required": true, + "schema": { + "type": "boolean" + } + }, + { + "name": "b", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "c", + "required": false, + "schema": { + "type": "number" + } + } + ] +} +``` + +Would result in the following method signatures: + +```typescript +listen(event: 'foo', a: boolean, callback: (b: string, c: number, data: any) => {}) + +listen(event: 'foo', a: boolean, b: string, callback: (c: number, data: any) => {}) + +listen(event: 'foo', a: boolean, b: string, c: number, callback: (data: any) => {}) +``` + +Which allows parameters to be omitted, from right-to-left, and included +as part of the result, instead. + +When invoking the callback, the SDK **MUST** pass the data portion of +the result to the data parameter of the callback, and each context +property to the corresponding callback parameter. + +This pattern also applies to property subscribers. diff --git a/requirements/specifications/intents/command-and-control.md b/requirements/specifications/intents/command-and-control.md new file mode 100644 index 000000000..db799cde9 --- /dev/null +++ b/requirements/specifications/intents/command-and-control.md @@ -0,0 +1,984 @@ +# Command and Control Intents + +Document Status: Proposed Specification + +See [Firebolt Requirements Governance](../../governance.md) for more info. + +| Contributor | Organization | +| ---------------- | ------------ | +| Saras Arveti | Comcast | +| Eileen Bengston | Comcast | +| Michael Driscoll | Comcast | +| Simon Grist | Sky | +| Jeremy LaCivita | Comcast | + +## 1. Overview + +This document outlines several basic Intents for controlling a Firebolt +compliant device. + +### 1.1. Message.type + +Message.type should be a useful grouping to bucket related intents +together for easier forwarding to appropriate components. + +## 2. Table of Contents +- [1. Overview](#1-overview) + - [1.1. Message.type](#11-messagetype) +- [2. Table of Contents](#2-table-of-contents) +- [3. Control Intents](#3-control-intents) + - [3.1. Power Intent](#31-power-intent) + - [3.2. Volume Intents](#32-volume-intents) + - [3.2.1. Volume Intent](#321-volume-intent) + - [3.2.2. Mute Intent](#322-mute-intent) + - [3.3. Channel Intent](#33-channel-intent) + - [3.4. Media Control Intents](#34-media-control-intents) + - [3.4.1. Pause, Play, Replay, and Stop Intents](#341-pause-play-replay-and-stop-intents) + - [3.4.2. Seek Intent](#342-seek-intent) + - [3.4.3. Fast-forward and Rewind Intents](#343-fast-forward-and-rewind-intents) + - [3.5. Accessibility Intents](#35-accessibility-intents) + - [3.5.1. Closed Captions Intent](#351-closed-captions-intent) + - [3.5.2. Voice Guidance Intent](#352-voice-guidance-intent) + - [3.5.3. Audio Descritions Intent](#353-audio-descritions-intent) + - [3.5.4. High Contrast Intent](#354-high-contrast-intent) + - [3.5.5. Screen Magnification Intent](#355-screen-magnification-intent) + - [3.6. Interaction Intents](#36-interaction-intents) + - [3.6.1. Focus Intent](#361-focus-intent) + - [3.6.2. Select Intent](#362-select-intent) + - [3.6.3. Scroll Intent](#363-scroll-intent) + - [3.6.4. Back Intent](#364-back-intent) + - [3.6.5. Exit Intent](#365-exit-intent) +- [4. Launch Intents](#4-launch-intents) + - [4.1. Content Discovery Launch Intents](#41-content-discovery-launch-intents) + - [4.2. Device Settings Launch Intent](#42-device-settings-launch-intent) + +## 3. Control Intents + +Control intents are for user intentions that will be needed regardless +of whether there are any apps installed. + +For example, these intents are all useful even if only using your TV +with a single HDMI input, and not for apps. + +### 3.1. Power Intent + +This intent allows a user to turn the device on or off. + +```json +{ + "type": "xrn:firebolt:intent:platform:power", + "target": "client", + "metadata": { + "assistant": "XFINITY", + "lang": "eng-USA", + "micType": "NEAR_FIELD" + }, + "intent": { + "action": "power", + "data": { + "value": true | false + }, + "context": { + "source": "voice" + } + } +} +``` + +Additionally, this intent may specify a toggle: + +```json +{ + "type": "xrn:firebolt:intent:platform:power", + "target": "client", + "metadata": { + "assistant": "XFINITY", + "lang": "eng-USA", + "micType": "NEAR_FIELD" + }, + "intent": { + "action": "power", + "data": { + "toggle": true + }, + "context": { + "source": "voice" + } + } +} +``` + +Additionally, this intent allows a user to set a timer for turning off +the power, aka a "sleep timer." + +This is handled by the optional field delay, which is measured in whole +seconds: + +```json +{ + "type": "xrn:firebolt:intent:platform:power", + "target": "client", + "metadata": { + "assistant": "XFINITY", + "lang": "eng-USA", + "micType": "NEAR_FIELD" + }, + "intent": { + "action": "power", + "data": { + "value": true | false, + "delay": 3600 + }, + "context": { + "source": "voice" + } + } +} +``` + +To cancel a sleep timer, send a new intent without a delay. + +While it may not be implemented by all platforms, this could also be +used to turn on the TV with a timer. + +### 3.2. Volume Intents + +Volume Intents control the audio level of the device. + +#### 3.2.1. Volume Intent + +This intent allows setting the volume to an absolute or relative value. + +```json +{ + "type": "xrn:firebolt:intent:platform:volume", + "target": "client", + "metadata": { + "assistant": "XFINITY", + "lang": "eng-USA", + "micType": "NEAR_FIELD" + }, + "intent": { + "action": "volume", + "data": { + "value": 70 + }, + "context": { + "source": "VOICE" + } + } +} + +``` + +The value is an integer value from 0 to 100. + +This intent also supports relative volume changes, by providing the +optional relative field: + +```json +{ + "type": "xrn:firebolt:intent:platform:volume", + "target": "client", + "metadata": { + "assistant": "XFINITY", + "lang": "eng-USA", + "micType": "NEAR_FIELD" + }, + "intent": { + "action": "volume", + "data": { + "value": -10, + "relative": true + }, + "context": { + "source": "VOICE" + } + } +} + +``` + +The value is a positive or negative integer that is relative to a scale +of 0-100. + +Firebolt will not support complicated relative changes, e.g. "Set the +volume to 50% *of what it currently is\...*" + +Firebolt uses a size of 0-100 for this intent. It\'s up to each voice +integration if it wants to convert "5" to "50%" before generating +the intent, but convenience transformations like this are recommended. + +Whether or not a TV uses logarithmic or linear scale is irrelevant to +the VolumeIntent schema. + +#### 3.2.2. Mute Intent + +This intent allows the user to mute or unmute the device. + +```json +{ + "type": "xrn:firebolt:intent:platform:volume", + "target": "client", + "metadata": { + "assistant": "XFINITY", + "lang": "eng-USA", + "micType": "NEAR_FIELD" + }, + "intent": { + "action": "mute", + "data": { + "value": true | false + }, + "context": { + "source": "VOICE" + } + } +} +``` + +### 3.3. Channel Intent + +For tuning to a specific channel, either OTA or in-app, see [Tune +Intents](./tune.md). + +The intents in this section are for relative next/previous channel user +intentions and are a separate type of Intent. This allows each app to +decide what "channel" means. For example, an App might simply take you +to the next section/genre if it doesn\'t have linear streams in it\'s +catalog. + +The goal of the action property to is tell the client how to parse the +Intent, so overloading the tune intent with a different structure is not +desirable. + +Also, +"tune" inherently means to zero in on a specific part of a +scale, e.g. tuning a harp. + +For relative "channel surfing" we\'ll use the more content-centric +action "channel" which will also align with non-linear apps that want +to leverage the channel up/down intent. + +The Channel Intent allows a user to scan "channels" in an app (or +actual OTA channels if not in an app). + +Users can scan to the next or previous channel. For scanning to the most +recent, i.e. "Last" channel, see [Interaction +Intent +](#interaction-intents). + +```json +{ + "type": "xrn:firebolt:intent:platform:channel", + "target": "client", + "metadata": { + "assistant": "XFINITY", + "lang": "eng-USA", + "micType": "NEAR_FIELD" + }, + "intent": { + "action": "channel", + "data": { + "value": "next" | "previous" + }, + "context": { + "source": "voice" + } + } +} + +``` + +The value property MUST always be "next" or "previous". These are +chosen over up/down since not all use cases will be numeric. + +Since this intent is always relative to the current app, there is no +need for an appId. + +If this Intent needs to be passed to the current app, it can be passed +as-is, via the Discovery. onNavigateTo API, or a simulated RCU press of +one of the channel up/down buttons. + +### 3.4. Media Control Intents + +#### 3.4.1. Pause, Play, Replay, and Stop Intents + +These intents allow the user to pause and resume playback of the current +Media: + +```json +{ + "type": "xrn:firebolt:intent:platform:media-control", + "target": "client", + "metadata": { + "assistant": "XFINITY", + "lang": "eng-USA", + "micType": "NEAR_FIELD" + }, + "intent": { + "action": "pause" | "play" | "replay" | "stop", + "context": { + "source": "voice" + } + } +} +``` + +If the action is pause, then the currently playing media should be +paused, with the frames on-screen and the video decoder ready to resume. + +If the action is play, and the current media is paused, then the +currently paused media should resume. + +If the action is play, and there is something playbable selected, then +playback of the selected asset should be initiated. + +If the action is replay, then the currently paused or playing media should restart +from the beginning. This should work even if the decoder has finished, +and its resources have been released. + +If the action is stop, then the currently playing media should be +stopped, frames removed from the screen, and any decoder resources +should be released. + +#### 3.4.2. Seek Intent + +The seek intent allows users to jump to a relative or absolute position +in the currently playing media. + +```json +{ + "type": "xrn:firebolt:intent:platform:media-control", + "target": "client", + "metadata": { + "assistant": "XFINITY", + "lang": "eng-USA", + "micType": "NEAR_FIELD" + }, + "intent": { + "action": "seek", + "data": { + "seconds": 3600 + }, + "context": { + "source": "voice" + } + } +} +``` + +The seconds value is a positive integer representing where to seek. + +This intent also supports relative seeking, by providing the optional +relative field: + +```json +{ + "type": "xrn:firebolt:intent:platform:media-control", + "target": "client", + "metadata": { + "assistant": "XFINITY", + "lang": "eng-USA", + "micType": "NEAR_FIELD" + }, + "intent": { + "action": "seek", + "data": { + "seconds": -30, + "relative": true + }, + "context": { + "source": "voice" + } + } +} +``` + +For relative seeking, the seconds value may be a positive or negative value. + +If a relative seek intent with a seconds value of `0` is received, the platform **SHOULD** ignore it, rather than rebuffering at the current position. + +#### 3.4.3. Fast-forward and Rewind Intents + +These intents allow users to fast-forward or rewind: + +```json +{ + "type": "xrn:firebolt:intent:platform:media-control", + "target": "client", + "metadata": { + "assistant": "XFINITY", + "lang": "eng-USA", + "micType": "NEAR_FIELD" + }, + "intent": { + "action": "fast-forward" | "rewind", + "data": { + "speed": 2.5 + }, + "context": { + "source": "voice" + } + } +} +``` + +Speed is a float in the range of 0 (non-includsive) to 10 (inclusive), +with values between 0 and 1 denoting slow motion. + +It is a device-level decision how to implement different speeds, however +actual fast playback (with audio) should be used where possible and +reasonable, e.g. a speed of 1.5 should actually be playing the video w/ +sync\'d audio, while a speed of 10 will likely be using iframes and not +have audio. For rewind it is not important, and likely undesirable, to +provide audio. + +If speed is not provided then the device should cycle through a range +of speeds defined by the device. This range of speeds **COULD** include +the value `1` so that users can get back to normal speed if desired. + +### 3.5. Accessibility Intents + +These intents manipulate accessibility features on the device. + +#### 3.5.1. Closed Captions Intent + +This intent allows a user to turn closed captions on or off. + +```json +{ + "type": "xrn:firebolt:intent:platform:accessibility", + "target": "client", + "metadata": { + "assistant": "XFINITY", + "lang": "eng-USA", + "micType": "NEAR_FIELD" + }, + "intent": { + "action": "closed-captions", + "data": { + "value": true | false + }, + "context": { + "source": "voice" + } + } +} +``` + +Additionally, this intent may specify a toggle: + +```json +{ + "type": "xrn:firebolt:intent:platform:accessibility", + "target": "client", + "metadata": { + "assistant": "XFINITY", + "lang": "eng-USA", + "micType": "NEAR_FIELD" + }, + "intent": { + "action": "closed-captions", + "data": { + "toggle": true + }, + "context": { + "source": "voice" + } + } +} +``` + +#### 3.5.2. Voice Guidance Intent + +This intent allows a user to turn voice guidance on or off. + +```json +{ + "type": "xrn:firebolt:intent:platform:accessibility", + "target": "client", + "metadata": { + "assistant": "XFINITY", + "lang": "eng-USA", + "micType": "NEAR_FIELD" + }, + "intent": { + "action": "voice-guidance", + "data": { + "value": true | false + }, + "context": { + "source": "voice" + } + } +} + +``` + +Additionally, this intent may specify a toggle: + +```json +{ + "type": "xrn:firebolt:intent:platform:accessibility", + "target": "client", + "metadata": { + "assistant": "XFINITY", + "lang": "eng-USA", + "micType": "NEAR_FIELD" + }, + "intent": { + "action": "voice-guidance", + "data": { + "toggle": true + }, + "context": { + "source": "voice" + } + } +} +``` + +The intent **MAY** specify `speed` `number` property that specifies a speed from 0 to 10: + +```json +{ + "type": "xrn:firebolt:intent:platform:accessibility", + "target": "client", + "metadata": { + "assistant": "XFINITY", + "lang": "eng-USA", + "micType": "NEAR_FIELD" + }, + "intent": { + "action": "voice-guidance", + "data": { + "speed": 2 + }, + "context": { + "source": "voice" + } + } +} +``` + +When providing a `speed` this intent **MAY** also set the `relative` property to `true` denoting an increase or decrease in speed. The speed value may be between -5 and 5 inclusive: + +```json +{ + "type": "xrn:firebolt:intent:platform:accessibility", + "target": "client", + "metadata": { + "assistant": "XFINITY", + "lang": "eng-USA", + "micType": "NEAR_FIELD" + }, + "intent": { + "action": "voice-guidance", + "data": { + "speed": -1, + "relative": true + }, + "context": { + "source": "voice" + } + } +} +``` + +Finally, the intent **MAY** specify a `verbosity` property, which **MUST** use one of the following values is provided: + +| Value | Description | +|--------|-------------| +| `low` | to select shorter response, less context, and less detail; can use abbreviations and can selectively skip words | +| `high` | to select longer response, more context, and more detail; full comprehensive readout and explicit reflection of what is seen on screen | + +```json +{ + "type": "xrn:firebolt:intent:platform:accessibility", + "target": "client", + "metadata": { + "assistant": "XFINITY", + "lang": "eng-USA", + "micType": "NEAR_FIELD" + }, + "intent": { + "action": "voice-guidance", + "data": { + "value": true, + "verbosity": "low" + }, + "context": { + "source": "voice" + } + } +} +``` + +#### 3.5.3. Audio Descritions Intent + +This intent allows a user to turn audio descriptions of content on or off. + + +```json +{ + "type": "xrn:firebolt:intent:platform:accessibility", + "target": "client", + "metadata": { + "assistant": "XFINITY", + "lang": "eng-USA", + "micType": "NEAR_FIELD" + }, + "intent": { + "action": "audio-descriptions", + "data": { + "value": true | false + }, + "context": { + "source": "voice" + } + } +} + +``` + +This intent may specify a language: + +```json +{ + "type": "xrn:firebolt:intent:platform:accessibility", + "target": "client", + "metadata": { + "assistant": "XFINITY", + "lang": "eng-USA", + "micType": "NEAR_FIELD" + }, + "intent": { + "action": "audio-descriptions", + "data": { + "value": true, + "language": "eng" + }, + "context": { + "source": "voice" + } + } +} +``` + +The `language` must be a three character ISO 639 1/2 code, e.g. `eng`. + +Additionally, this intent may specify a toggle: + +```json +{ + "type": "xrn:firebolt:intent:platform:accessibility", + "target": "client", + "metadata": { + "assistant": "XFINITY", + "lang": "eng-USA", + "micType": "NEAR_FIELD" + }, + "intent": { + "action": "audio-descriptions", + "data": { + "toggle": true + }, + "context": { + "source": "voice" + } + } +} +``` + +#### 3.5.4. High Contrast Intent + +This intent allows a user to turn high contrast mode on or off. + +```json +{ + "type": "xrn:firebolt:intent:platform:accessibility", + "target": "client", + "metadata": { + "assistant": "XFINITY", + "lang": "eng-USA", + "micType": "NEAR_FIELD" + }, + "intent": { + "action": "high-contrast", + "data": { + "value": true | false + }, + "context": { + "source": "voice" + } + } +} + +``` + +Additionally, this intent may specify a toggle: + +```json +{ + "type": "xrn:firebolt:intent:platform:accessibility", + "target": "client", + "metadata": { + "assistant": "XFINITY", + "lang": "eng-USA", + "micType": "NEAR_FIELD" + }, + "intent": { + "action": "high-contrast", + "data": { + "toggle": true + }, + "context": { + "source": "voice" + } + } +} +``` + +#### 3.5.5. Screen Magnification Intent + +This intent allows a user to turn screen magnification on or off. + +```json +{ + "type": "xrn:firebolt:intent:platform:accessibility", + "target": "client", + "metadata": { + "assistant": "XFINITY", + "lang": "eng-USA", + "micType": "NEAR_FIELD" + }, + "intent": { + "action": "screen-magnification", + "data": { + "value": true | false + }, + "context": { + "source": "voice" + } + } +} + +``` + +Additionally, this intent may specify a toggle: + +```json +{ + "type": "xrn:firebolt:intent:platform:accessibility", + "target": "client", + "metadata": { + "assistant": "XFINITY", + "lang": "eng-USA", + "micType": "NEAR_FIELD" + }, + "intent": { + "action": "screen-magnification", + "data": { + "toggle": true + }, + "context": { + "source": "voice" + } + } +} +``` + +Finally, this intent may specify a magnification scale as a number: + +```json +{ + "type": "xrn:firebolt:intent:platform:accessibility", + "target": "client", + "metadata": { + "assistant": "XFINITY", + "lang": "eng-USA", + "micType": "NEAR_FIELD" + }, + "intent": { + "action": "screen-magnification", + "data": { + "scale": 2.5 + }, + "context": { + "source": "voice" + } + } +} +``` + +Setting the scale to `1` turns off magnification. Setting the scale to a value greater than 1 turns on magnification. + +Even if a Firebolt platform does not support specifying the numeric scale, it **MUST** turn magnifacation on and off based on them. + +If the intent has the `toggle` property, then it **MUST NOT** have the `scale` or `value` property. + +If the intent has the `value` property, then it **MUST NOT** have the `toggle`. + +### 3.6. Interaction Intents + +Interaction Intents allow for voice (or other upstream intent service) +to control an on-screen UI without need for a keyboard or remote. + +#### 3.6.1. Focus Intent + +The Focus Intent allows users to move the focus / cursor +up/down/left/right: + +```json +{ + "type": "xrn:firebolt:intent:platform:interaction", + "target": "client", + "metadata": { + "assistant": "XFINITY", + "lang": "eng-USA", + "micType": "NEAR_FIELD" + }, + "intent": { + "action": "focus", + "data": { + "direction": "up" | "down" | "left" | "right" + }, + "context": { + "source": "voice" + } + } +} + +``` + +Note that this does not give focus to a particular app, which is handled +by the "launch" action. + +These Intents will generate appropriate HTML browser keyCode events to +facilitate up/down/left/right key presses. + +#### 3.6.2. Select Intent + +The select intent allows users to tell an app select, e.g., +"click" on +whatever is focused. This is a platform-level intent that effectively +sends the "Ok" or "Select" key to the current app. + +```json +{ + "type": "xrn:firebolt:intent:platform:interaction", + "target": "client", + "metadata": { + "assistant": "XFINITY", + "lang": "eng-USA", + "micType": "NEAR_FIELD" + }, + "intent": { + "action": "select", + "context": { + "source": "voice" + } + } +} +``` + +#### 3.6.3. Scroll Intent + +The Scroll Intent allows users to move the current view port +up/down/left/right: + +```json +{ + "type": "xrn:firebolt:intent:platform:interaction", + "target": "client", + "metadata": { + "assistant": "XFINITY", + "lang": "eng-USA", + "micType": "NEAR_FIELD" + }, + "intent": { + "action": "scroll", + "data": { + "direction": "up" | "down" | "left" | "right", + "unit": "page" | "line" | "percent" + }, + "context": { + "source": "voice" + } + } +} +``` + +Both `direction` and `unit` are required. + +These Intents will generate appropriate browser / DOM scrolling +operations that don\'t require custom APIs. + +#### 3.6.4. Back Intent + +The back intent allows users to tell an app go to "back" like a +browser. This is a platform-level intent and will initiate a browser +back flow for web apps. For native apps, this will be converted to an +app Navigation Intent by the client and surfaced through the navigateTo +API. + +```json +{ + "type": "xrn:firebolt:intent:platform:interaction", + "target": "client", + "metadata": { + "assistant": "XFINITY", + "lang": "eng-USA", + "micType": "NEAR_FIELD" + }, + "intent": { + "action": "back", + "context": { + "source": "voice" + } + } +} +``` + +#### 3.6.5. Exit Intent + +The exit intent allows users to tell an app close. This is a +platform-level intent and will simply move the current app into the +inactive state. + +```json +{ + "type": "xrn:firebolt:intent:platform:interaction", + "target": "client", + "metadata": { + "assistant": "XFINITY", + "lang": "eng-USA", + "micType": "NEAR_FIELD" + }, + "intent": { + "action": "exit", + "context": { + "source": "voice" + } + } +} +``` + +## 4. Launch Intents + +If a Firebolt app wants to launch the main or settings experience of the device, it can use one of the following abstract appIds with the `launch` intent. + +### 4.1. Content Discovery Launch Intents + +The following section IDs will be used, with the Firebolt application +type as the target App ID: + +`xrn:firebolt:application-type:main` + +### 4.2. Device Settings Launch Intent + +To launch the settings UI, a Launch Intent will be used, with the +Firebolt application type: + +`xrn:firebolt:application-type:settings` diff --git a/requirements/specifications/intents/index.md b/requirements/specifications/intents/index.md index 6925376e4..72d6138c8 100644 --- a/requirements/specifications/intents/index.md +++ b/requirements/specifications/intents/index.md @@ -7,6 +7,8 @@ See [Firebolt Requirements Governance](../../governance.md) for more info. | Contributor | Organization | | --------------- | ------------ | | Jeremy LaCivita | Comcast | +| Simon Grist | Sky | + ## 1. Overview Offen times an end-user has a specific intention that needs to be communicated @@ -27,18 +29,23 @@ additional `data` property. - [3. Intent Action](#3-intent-action) - [4. Intent Context](#4-intent-context) - [5. Intent Data](#5-intent-data) -- [Intent Types](#intent-types) +- [6. Intent Message](#6-intent-message) + - [6.1. App Intent Message](#61-app-intent-message) + - [6.2. Platform Intent Message](#62-platform-intent-message) + - [6.3. Intent Message Type](#63-intent-message-type) + - [6.4. Intent Message Metadata](#64-intent-message-metadata) +- [7. Intent Types](#7-intent-types) ## 3. Intent Action The intent `action` denotes what type of intent it is. -All intents **MUST** have a `string` attribute denoting the type of intent. +All intents **MUST** have an `action` `string` property denoting the type of intent. See the various [Intent Types](#intent-types) below for values. ## 4. Intent Context -The intent `context` provides information on where the intent orginated from. +The intent `context` provides information on where the intent orginated from. All intents **MUST** have a `context` property, which is an object. The `context` object **MUST** have a `source` string property with one of the @@ -56,7 +63,56 @@ any string value. This property denotes an editorial campaign. ## 5. Intent Data If an intent has any additional data, it **MUST** be in the `data` property. -## Intent Types +## 6. Intent Message +When an intent is sent to a Firebolt device from some other system, e.g. a cloud voice service, it **MUST** be wrapped in an `IntentMessage` object so that it can be properly routed after transport. + +An intent message **MUST** have an `intent` object property that is the intent being passed. + +An example intent message: + +```json +{ + "type": "xrn:firebolt:intent:app:launch", + "appId": "Netflix", + "intent": { + "action": "launch", + "context": { + "source": "voice" + } + }, + "metadata": { + "foo": "bar" + } +} +``` + +### 6.1. App Intent Message +If an intent is targeting a specific app, then the intent message **MUST** have an `appId` string property with the appId of the targeted app. + +### 6.2. Platform Intent Message +If an intent messagage does not have an `appId` property, then it **MUST** be targeting the device itself, e.g. a `power` intent to turn off the device. + +### 6.3. Intent Message Type +An intent message **MUST** have a `type` string property for categorizing the intent. + +The type property **MUST** match the regular expression: + +```regex +^xrn:firebolt:intent:(app|platform):[a-zA-Z]+$ +``` + +App Intent Messages **MUST** have the fourth section set to `app`. + +Platform Intent Messages **MUST** have the fourth section set to `platform` + +All Intent Messages **MUST** have the fifth section set to the same value as `intent.action`. + +Platforms may use this to route different types of intents to different subsystems without having to understand the internal structure of Firebolt intent objects. + +### 6.4. Intent Message Metadata +An intent message **MAY** have a `metadata` object property for adding distributor-specific metadata for logging or analytics. The values in `metadata` **MUST NOT** impact how the device interprets the intent. + +## 7. Intent Types - [Play](./play.md) - [Tune](./tune.md) diff --git a/requirements/specifications/intents/user-interest.md b/requirements/specifications/intents/user-interest.md new file mode 100644 index 000000000..eae60ecf7 --- /dev/null +++ b/requirements/specifications/intents/user-interest.md @@ -0,0 +1,15 @@ +# User Interest + +Document Status: Candidate Specification + +See [Firebolt Requirements Governance](../../governance.md) for more info. + +| Contributor | Organization | +| -------------- | -------------- | +| Eugene Chung | Comcast | +| Tim Dibben | Sky | +| Mike Horwitz | Comcast | +| Jeremy LaCivita | Comcast | + +## 1. Overview +This document describes the intent to initiate a [User Interest](../discovery/user-interest.md) flow from an upstream system, e.g. a voice assistant. diff --git a/requirements/specifications/openrpc-extensions/app-passthrough-apis.md b/requirements/specifications/openrpc-extensions/app-passthrough-apis.md new file mode 100644 index 000000000..ec23922e6 --- /dev/null +++ b/requirements/specifications/openrpc-extensions/app-passthrough-apis.md @@ -0,0 +1,477 @@ +# App Pass-through APIs + +Document Status: Working Draft + +See [Firebolt Requirements Governance](../../governance.md) for more info. + +| Contributor | Organization | +|-----------------|----------------| +| Jeremy LaCivita | Comcast | +| Kevin Pearson | Comcast | +| Yuri Pasquali | Sky | + +## 1. Overview +This document describes how one Firebolt App can provide a capability that may be used by another Firebolt App, with the platform as a permission broker that passes the requests and responses to each app without feature-specific logic. + +This document covers the App Pass-through Firebolt OpenRPC extension as well as how Firebolt implementations should detect and execute app provided pass-through APIs. + +Some APIs require an app to fulfill the request on behalf of another app, e.g. to provide a UX or cross-app data sharing. Generally the calling app doesn't care, or have a say in, which other app provides the API, that is up to the Firebolt distributor. + +To facilitate these APIs, Firebolt denotes an OpenRPC tag with OpenRPC extensions to connect the `provide` API to the `use` API. + +This document is written using the [IETF Best Common Practice 14](https://www.rfc-editor.org/rfc/rfc2119.txt) and should include the following summary in the Overview section: + +The key words "**MUST**", "**MUST NOT**", "**REQUIRED**", "**SHALL**", "**SHALL NOT**", "**SHOULD**", "**SHOULD NOT**", "**RECOMMENDED**", "**NOT RECOMMENDED**", "**MAY**", and "**OPTIONAL**" in this document are to be interpreted as described in [BCP 14](https://www.rfc-editor.org/rfc/rfc2119.txt) [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here. + +## 2. Table of Contents +- [1. Overview](#1-overview) +- [2. Table of Contents](#2-table-of-contents) +- [3. Open RPC Extensions](#3-open-rpc-extensions) + - [3.1. Provided By Extension](#31-provided-by-extension) +- [4. Routing App pass-through APIs](#4-routing-app-pass-through-apis) + - [4.1. No available providers](#41-no-available-providers) + - [4.2. Direct pass-through](#42-direct-pass-through) + - [4.4. Pass-through notifications](#44-pass-through-notifications) +- [5. Provider Candidates](#5-provider-candidates) +- [6. Best Candidate](#6-best-candidate) +- [7. Result Transformations](#7-result-transformations) +- [8. Provider Parameter Injection](#8-provider-parameter-injection) +- [9. API Gateway](#9-api-gateway) +- [10. Example: User Interest](#10-example-user-interest) + - [10.1. User Interest Pull](#101-user-interest-pull) + - [10.2. User Interest Push](#102-user-interest-push) + +## 3. Open RPC Extensions + +### 3.1. Provided By Extension +Firebolt OpenRPC **MUST** support a `string` `x-provided-by` extension property on the `capabilities` tag that denotes a method is provided by some app on the device registering for the specified provider API, e.g.: + +```json +{ + "methods": [ + { + "name": "Keyboard.standard", + "tags": [ + { + "name": "capabilities", + "x-provided-by": "Keyboard.onRequestStandard", + "x-uses": [ + "xrn:firebolt:capability:input:keyboard" + ] + } + ] + } + ] +} +``` + +The method denoted by `x-provided-by` is referred to as the "*provider*" or "*provider method*" for the remainder of this document. + +The method with the `x-provided-by` extension is referred to as the "*platform method*" for the remainder of this document. + +To prevent unresolvable chaining of methods the `x-provided-by` extension **MUST NOT** be used on a method with any value in the `x-provides` extension. + +To prevent compound methods a platform method **MUST** `use` a single capability or `manage` a single capability, but not both. + +The provider method **MUST** provide the same capability that the platform method either uses or manages. + +If a platform method has no provider method then it is not a valid Firebolt OpenRPC method schema, and a validation error **MUST** be generated. + +## 4. Routing App pass-through APIs +App pass-through APIs may be routed in one of several ways. + +When an app calls a platform method, i.e. one with an `x-provided-by` extension, the platform **MUST** use one of the routing methods defined in this section based on various properties of the method. + +### 4.1. No available providers +When an app calls a platform method with an `x-provided-by` extension, the platform **MUST** return an unavailable error if there is no [candidate app](#7-provider-candidates) to execute the provider method. + +```json +{ + "id": 1, + "error": { + "code": -50300, + "message": "Capability is unavailable." + } +} +``` + +Where `` is the capability XRN string, e.g. `xrn:firebolt:capabilities:example:foo`. + +### 4.2. Direct pass-through +A direct pass-through is where a single app provides a single response to a single request by another app. + +This section only applies to app provider methods that do not have an `event` tag. + +The platform method result schema **MUST** either: + +> Match the `x-response` schema on the provider method so that the result can be passed through. +> +> or +> +> Have a property that matches the `x-response-name` string and `x-response` schema on the +> provider method so that the result can be composed and passed through. + +The platform **MUST** call the provider method from the [best candidate](#8-best-candidate) app and acquire the result. + +If the platform method result schema matches the `x-response` schema on the provider method then the value **MUST** be used as-is. + +Otherwise if the platform method result schema has a property that matches the `x-response` schema on the provider method then the value **MUST** be composed into an object under the corresponding property name and the platform **MUST** apply any [result transformations](#9-result-transformations) to the composed result. + +### 4.4. Pass-through notifications +Firebolt events have a synchronous subscriber registration method, e.g. `Lifecycle.onInactive(true)`, in addition to asynchronous notifications when the event actually happens. For events powered by an app pass-through, only the asynchronous notifications are passed in by the providing app. The initial event registration is handled by the platform, and the success response is not handled by the providing app. + +This section only applies to platform methods that have an `event` tag. + +App provided event registration **MUST** not return an availability error due to a lack of providers, since one may be launched at a future point. + +To ensure that event provider methods all behave the same the provider method **MUST** have a `result` schema with `"type"` set to `"null"`, since it will not expect any data in the response from the platform after pushing the notification. + +The platform method result schema **MUST** either: + +> Match the *last* parameter schema on the provider method so that the result can be passed through. +> +> Have a property that matches the *last* parameter name and schema on the provider method so that the result can be passed through. + +Example platform method with context: +```json +{ + "name": "onFoo", + "tags": [ + { + "name": "capabilities", + "x-uses": [ + "xrn:firebolt:capabilities:example:foo" + ], + "x-provided-by": "foo" + }, + { + "name": "event" + } + ], + "params": [ + { + "name": "context1", + "schema":{ + "type": "string" + } + }, + { + "name": "context2", + "schema": { + "type": "number" + } + } + ], + "result": { + "name": "value", + "schema": { + "type": "boolean" + } + } +} +``` + +Matching provider method: + +```json +{ + "name": "foo", + "tags": [ + { + "name": "capabilities", + "x-provides": "xrn:firebolt:capabilities:example:foo" + } + ], + "params": [ + { + "name": "context1", + "schema":{ + "type": "string" + } + }, + { + "name": "context2", + "schema": { + "type": "number" + } + }, + { + "name": "value", + "schema": { + "type": "boolean" + } + } + ] +} +``` + +When a provider app calls a provider method mapped to an event the platform **MUST** ignore the notification if the app is not a [candidate app](#7-provider-candidates) for this capability. + +If the platform method result schema matches the *last* parameter schema on the provider method then the value **MUST** be used as-is. + +Otherwise if the platform method result schema has a property that matches the *last* parameter schema on the provider method then the value **MUST** be composed into an object under the corresponding property name and the platform **MUST** apply any [result transformations](#9-result-transformations) to the composed result. + +If the value was composed into the platform method result under a matching property, then any context parameter values from the provider method that correspond to a property name and schema in the platform method result **MUST** also be composed into the platform method result under those properties. + +Finally the platform **MUST** dispatch the notification to the app that registered for the event via the original platform method, using all but the last parameter as context. + +## 5. Provider Candidates +The Firebolt Device Manifist **MUST** have a list of `ProviderPolicy` configurations that map capabilities to policies for determining candidate providers: + +```json +{ + "providerPolicies": [ + { + "inFocus": true, + "capabilities": [ + "xrn:firebolt:capability:foo:bar" + ] + } + ] +} +``` +The policy **MUST** have a list of capabilities that it is applied to. + +A capability **MUST NOT** be included in more than one policy. + +The policy **MAY** have an `inFocus` boolean. + +If the policy has `inFocus` set to `true` then any app without RCU input focus when the capability is invoked **MUST NOT** be considered a candidate. + +## 6. Best Candidate +If there is only one candidate then it **MUST** be the best candidate. + +If there is more than one candidate, then the candidate app that most recently had RCU input focus **MUST** be the best candidate. + +If none of the candidates have had focus yet, then the candidate app that was most recently launched **MUST** be the best candidate. + +## 7. Result Transformations +A platform method may be configured to insert the providing app id into composite values. This is not allowed in non-composite results to avoid collisions with the provder method sending an appId and Firebolt overriding it. + +If a "composite result" was used to wrap the provider method value and the platform method's schema has an `appId` `string` property at the top level then the property's value **MUST** be set to the the appId of the providing app for that result. + +## 8. Provider Parameter Injection +If the provider method has a parameter named `appId` and the platform method *does not*, then the appId of the app calling the platform method **MUST** be sent to the provider in the `appId` parameter. + +If the platform method is an `event` and the provider method has context parameters then each context parameter from the provider that has a matching context parameter in the event **MUST** have it's value passed to that parameter. + +If the platform method is an `event` with a "composite result" and the provider method has context parameters then each context parameter from the provider that has a matching property on the `result` object **MUST** have it's value copied into that property. + +## 9. API Gateway +The Firebolt API Gateway **MUST** detect app-passthrough APIs and map the `use`/`manage` APIs to the corresponding `provide` APIs by parsing the Firebolt OpenRPC Specification and following the logic outline in this document. + +## 10. Example: User Interest + +The following schemas are referenced by these examples: + +```json +{ + "components": { + "schemas": { + "InterestType": { + "type": "string", + "enum": [ + "interest", + "disinterest" + ] + }, + "InterestReason": { + "type": "string", + "enum": [ + "playlist" + ] + }, + "EntityDetailsFromApp": { + "type": "object", + "properties": { + "appId": { + "type": "string" + }, + "entity": { + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/EntityDetails" + } + }, + "required": [ + "appId", + "entity" + ] + } + } + } +} +``` + +### 10.1. User Interest Pull + +Platform method: + +```json +{ + "methods": [ + { + "name": "requestUserInterest", + "tags": [ + { + "name": "capabilities", + "x-provided-by": "Discovery.onRequestUserInterest", + "x-uses": [ + "xrn:firebolt:capability:discovery:interest" + ] + } + ], + "params": [ + { + "name": "type", + "required": true, + "schema": { + "$ref": "#/components/schemas/InterestType" + } + }, + { + "name": "reason", + "required": true, + "schema": { + "$ref": "#/components/schemas/InterestReason" + } + } + ], + "result": { + "name": "interest", + "schema": { + "$ref": "#/components/schemas/EntityDetailsFromApp", + } + } + } + ] +} +``` + +Provider method: + +```json +{ + "methods": [ + { + "name": "onRequestUserInterest", + "tags": [ + { + "name": "capabilities", + "x-provides": "xrn:firebolt:capability:discovery:interest" + }, + { + "name": "event", + "x-response": { + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/EntityDetails" + } + } + ], + "result": { + "name": "request", + "schema": { + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/InterestType", + }, + "reason": { + "$ref": "#/components/schemas/InterestReason", + } + } + } + } + } + ] +} +``` + +### 10.2. User Interest Push + +Provider method: + +```json +{ + "methods": [ + { + "name": "userInterest", + "tags": [ + { + "name": "capabilities", + "x-provides": "xrn:firebolt:capability:discovery:interest" + } + ], + "params": [ + { + "name": "type", + "schema": { + "$ref": "#/components/schemas/InterestType", + } + }, + { + "name": "reason", + "schema": { + "$ref": "#/components/schemas/InterestReason", + } + }, + { + "name": "entity", + "schema": { + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/EntityDetails" + } + } + ], + "result": { + "name": "result", + "schema": { + "type": "null" + } + } + } + ] +} +``` + +Platform Method: + +```json +{ + "methods": [ + { + "name": "onUserInterest", + "tags": [ + { + "name": "capabilities", + "x-provided-by": "Discovery.userInterest", + "x-uses": [ + "xrn:firebolt:capability:discovery:interest" + ] + }, + { + "name": "event" + } + ], + "params": [], + "result": { + "name": "interest", + "schema": { + "type": "object", + "properties": { + "appId": { + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/InterestType" + }, + "reason": { + "$ref": "#/components/schemas/InterestReason" + }, + "entity": { + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/EntityDetails" + } + } + + } + } + } + ] +} +``` diff --git a/src/openrpc/content.json b/src/openrpc/content.json new file mode 100644 index 000000000..0ffbeadbf --- /dev/null +++ b/src/openrpc/content.json @@ -0,0 +1,195 @@ +{ + "openrpc": "1.2.4", + "info": { + "title": "Content", + "version": "0.0.0", + "description": "" + }, + "methods": [ + { + "name": "requestUserInterest", + "tags": [ + { + "name": "capabilities", + "x-provided-by": "Discovery.onRequestUserInterest", + "x-uses": [ + "xrn:firebolt:capability:discovery:interest" + ] + } + ], + "summary": "Provide information about the entity currently displayed or selected on the screen.", + "description": "Provide information about the entity currently displayed or selected on the screen.", + "params": [ + { + "name": "type", + "required": true, + "schema": { + "$ref": "https://meta.comcast.com/firebolt/discovery#/definitions/InterestType" + } + }, + { + "name": "reason", + "required": true, + "schema": { + "$ref": "https://meta.comcast.com/firebolt/discovery#/definitions/InterestReason" + } + } + ], + "result": { + "name": "interest", + "schema": { + "$ref": "#/components/schemas/InterestResult" + }, + "summary": "The EntityDetails data." + }, + "examples": [ + { + "name": "Default Example", + "params": [ + { + "name": "type", + "value": "interest" + }, + { + "name": "reason", + "value": "playlist" + } + ], + "result": { + "name": "interest", + "value": { + "appId": "cool-app", + "entity": { + "identifiers": { + "entityId": "345", + "entityType": "program", + "programType": "movie" + }, + "info": { + "title": "Cool Runnings", + "synopsis": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Pulvinar sapien et ligula ullamcorper malesuada proin libero nunc.", + "releaseDate": "1993-01-01T00:00:00.000Z", + "contentRatings": [ + { + "scheme": "US-Movie", + "rating": "PG" + }, + { + "scheme": "CA-Movie", + "rating": "G" + } + ] + } + } + } + } + } + ] + }, + { + "name": "onUserInterest", + "tags": [ + { + "name": "event" + }, + { + "name": "capabilities", + "x-provided-by": "Discovery.userInterest", + "x-uses": [ + "xrn:firebolt:capability:discovery:interest" + ] + } + ], + "summary": "Provide information about the entity currently displayed or selected on the screen.", + "description": "Provide information about the entity currently displayed or selected on the screen.", + "params": [], + "result": { + "name": "interest", + "schema": { + "$ref": "#/components/schemas/InterestEvent" + }, + "summary": "The EntityDetails data." + }, + "examples": [ + { + "name": "Default Example", + "params": [], + "result": { + "name": "interest", + "value": { + "appId": "cool-app", + "type": "interest", + "reason": "playlist", + "entity": { + "identifiers": { + "entityId": "345", + "entityType": "program", + "programType": "movie" + }, + "info": { + "title": "Cool Runnings", + "synopsis": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Pulvinar sapien et ligula ullamcorper malesuada proin libero nunc.", + "releaseDate": "1993-01-01T00:00:00.000Z", + "contentRatings": [ + { + "scheme": "US-Movie", + "rating": "PG" + }, + { + "scheme": "CA-Movie", + "rating": "G" + } + ] + } + } + } + } + } + ] + } + ], + "components": { + "schemas": { + "InterestResult": { + "title": "InterestResult", + "type": "object", + "properties": { + "appId": { + "type": "string" + }, + "entity": { + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/EntityDetails" + } + }, + "required": [ + "appId", + "entity" + ] + }, + "InterestEvent": { + "title": "InterestEvent", + "type": "object", + "properties": { + "appId": { + "type": "string" + }, + "type": { + "$ref": "https://meta.comcast.com/firebolt/discovery#/definitions/InterestType" + }, + "reason": { + "$ref": "https://meta.comcast.com/firebolt/discovery#/definitions/InterestReason" + }, + "entity": { + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/EntityDetails" + } + }, + "required": [ + "appId", + "entity", + "type", + "reason" + ] + } + } + } +} \ No newline at end of file diff --git a/src/openrpc/discovery.json b/src/openrpc/discovery.json index 8c620271a..eed6b3611 100644 --- a/src/openrpc/discovery.json +++ b/src/openrpc/discovery.json @@ -52,6 +52,10 @@ { "name": "capabilities", "x-provides": "xrn:firebolt:capability:discovery:entity-info" + }, + { + "name": "deprecated", + "x-alternative": "Discovery.details" } ], "summary": "Provide information about a program entity and its available watchable assets, such as entitlement status and price, via either a push or pull call flow.", @@ -409,7 +413,7 @@ } ], "result": { - "name": "success", + "name": "result", "value": true } } @@ -424,6 +428,10 @@ { "name": "capabilities", "x-provides": "xrn:firebolt:capability:discovery:purchased-content" + }, + { + "name": "deprecated", + "x-alternative": "Discovery.purchases" } ], "summary": "Provide a list of purchased content for the authenticated account, such as rentals and electronic sell through purchases.", @@ -636,7 +644,7 @@ "name": "identifiers", "summary": "A set of content identifiers for this call to action", "schema": { - "$ref": "https://meta.comcast.com/firebolt/entertainment#/definitions/ContentIdentifiers" + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/Entity" }, "required": true }, @@ -1467,6 +1475,156 @@ } } ] + }, + { + "name": "userInterest", + "summary": "Send an entity that the user has expressed interest in to the platform.", + "tags": [ + { + "name": "capabilities", + "x-provides": "xrn:firebolt:capability:discovery:interest" + } + ], + "params": [ + { + "name": "type", + "required": true, + "schema": { + "$ref": "https://meta.comcast.com/firebolt/discovery#/definitions/InterestType" + } + }, + { + "name": "reason", + "required": true, + "schema": { + "$ref": "https://meta.comcast.com/firebolt/discovery#/definitions/InterestReason" + } + }, + { + "name": "entity", + "required": true, + "schema": { + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/EntityDetails" + } + } + ], + "result": { + "name": "result", + "schema": { + "type": "null" + } + }, + "examples": [ + { + "name": "Default Example", + "params": [ + { + "name": "type", + "value": "interest" + }, + { + "name": "reason", + "value": "playlist" + }, + { + "name": "entity", + "value": { + "identifiers": { + "entityId": "345", + "entityType": "program", + "programType": "movie" + }, + "info": {} + } + } + ], + "result": { + "name": "result", + "value": null + } + } + ] + }, + { + "name": "Discovery.onRequestUserInterest", + "tags": [ + { + "name": "rpc-only" + }, + { + "name": "event", + "x-response-name": "entity", + "x-response": { + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/EntityDetails", + "examples": [ + { + "identifiers": { + "entityId": "345", + "entityType": "program", + "programType": "movie" + }, + "info": { + "title": "Cool Runnings", + "synopsis": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Pulvinar sapien et ligula ullamcorper malesuada proin libero nunc.", + "releaseDate": "1993-01-01T00:00:00.000Z", + "contentRatings": [ + { + "scheme": "US-Movie", + "rating": "PG" + }, + { + "scheme": "CA-Movie", + "rating": "G" + } + ] + } + } + ] + } + }, + { + "name": "capabilities", + "x-provides": "xrn:firebolt:capability:discovery:interest" + } + ], + "summary": "Provide information about the entity currently displayed or selected on the screen.", + "description": "Provide information about the entity currently displayed or selected on the screen.", + "params": [], + "result": { + "name": "request", + "schema": { + "type": "object", + "required": [ + "correlationId", + "parameters" + ], + "properties": { + "correlationId": { + "type": "string" + }, + "parameters": { + "$ref": "#/components/schemas/UserInterestProviderParameters" + } + }, + "additionalProperties": false + } + }, + "examples": [ + { + "name": "Default Example", + "params": [], + "result": { + "name": "request", + "value": { + "correlationId": "xyz", + "parameters": { + "type": "interest", + "reason": "playlist" + } + } + } + } + ] } ], "components": { @@ -1785,6 +1943,22 @@ "enum": [ "xrn:firebolt:channel:any" ] + }, + "UserInterestProviderParameters": { + "title": "UserInterestProviderParameters", + "type": "object", + "required": [ + "type", + "reason" + ], + "properties": { + "type": { + "$ref": "https://meta.comcast.com/firebolt/discovery#/definitions/InterestType" + }, + "reason": { + "$ref": "https://meta.comcast.com/firebolt/discovery#/definitions/InterestReason" + } + } } } } diff --git a/src/openrpc/keyboard.json b/src/openrpc/keyboard.json index 0ef1f725b..9d8e39412 100644 --- a/src/openrpc/keyboard.json +++ b/src/openrpc/keyboard.json @@ -11,6 +11,7 @@ "tags": [ { "name": "capabilities", + "x-provided-by": "Keyboard.onRequestEmail", "x-uses": [ "xrn:firebolt:capability:input:keyboard" ] @@ -84,6 +85,7 @@ "tags": [ { "name": "capabilities", + "x-provided-by": "Keyboard.onRequestPassword", "x-uses": [ "xrn:firebolt:capability:input:keyboard" ] @@ -128,6 +130,7 @@ "tags": [ { "name": "capabilities", + "x-provided-by": "Keyboard.onRequestStandard", "x-uses": [ "xrn:firebolt:capability:input:keyboard" ] @@ -175,11 +178,9 @@ { "name": "event", "x-response": { - "$ref": "#/components/schemas/KeyboardResult", + "type": "string", "examples": [ - { - "text": "username" - } + "username" ] } }, @@ -220,11 +221,9 @@ { "name": "event", "x-response": { - "$ref": "#/components/schemas/KeyboardResult", + "type": "string", "examples": [ - { - "text": "password" - } + "password" ] } }, @@ -265,11 +264,9 @@ { "name": "event", "x-response": { - "$ref": "#/components/schemas/KeyboardResult", + "type": "string", "examples": [ - { - "text": "email@address.com" - } + "email@address.com" ] } }, @@ -359,23 +356,6 @@ "$ref": "#/components/schemas/KeyboardParameters" } } - }, - "KeyboardResult": { - "title": "KeyboardResult", - "type": "object", - "required": [ - "text" - ], - "properties": { - "text": { - "type": "string", - "description": "The text the user entered into the keyboard" - }, - "canceled": { - "type": "boolean", - "description": "Whether the user canceled entering text before they were finished typing on the keyboard" - } - } } } } diff --git a/src/schemas/advertising.json b/src/schemas/advertising.json new file mode 100644 index 000000000..e3aca3ede --- /dev/null +++ b/src/schemas/advertising.json @@ -0,0 +1,23 @@ +{ + "$id": "https://meta.comcast.com/firebolt/advertising", + "title": "Advertising", + "oneOf": [ + { + "$ref": "#/definitions/SkipRestriction" + } + ], + "definitions": { + "SkipRestriction": { + "title": "SkipRestriction", + "$comment": "xrn:advertising:policy:skipRestriction:", + "type": "string", + "enum": [ + "none", + "adsUnwatched", + "adsAll", + "all" + ], + "description": "The advertisement skip restriction.\n\nApplies to fast-forward/rewind (e.g. trick mode), seeking over an entire opportunity (e.g. jump), seeking out of what's currently playing, and \"Skip this ad...\" features. Seeking over multiple ad opportunities only requires playback of the _last_ opportunity, not all opportunities, preceding the seek destination.\n\n| Value | Description |\n|--------------|--------------------------------------------------------------------------------|\n| none |No fast-forward, jump, or skip restrictions |\n| adsUnwatched | Restrict fast-forward, jump, and skip for unwatched ad opportunities only. |\n| adsAll | Restrict fast-forward, jump, and skip for all ad opportunities |\n| all | Restrict fast-forward, jump, and skip for all ad opportunities and all content |\n\nNamespace: `xrn:advertising:policy:skipRestriction:`\n\n" + } + } +} \ No newline at end of file diff --git a/src/schemas/discovery.json b/src/schemas/discovery.json new file mode 100644 index 000000000..1c13435a7 --- /dev/null +++ b/src/schemas/discovery.json @@ -0,0 +1,79 @@ +{ + "$id": "https://meta.comcast.com/firebolt/discovery", + "title": "Discovery", + "anyOf": [ + { + "$ref": "#/definitions/PurchasedContentResult" + } + ], + "definitions": { + "PurchasedContentResult": { + "title": "PurchasedContentResult", + "type": "object", + "properties": { + "expires": { + "type": "string", + "format": "date-time" + }, + "totalCount": { + "type": "integer", + "minimum": 0 + }, + "entries": { + "type": "array", + "items": { + "$ref": "https://meta.comcast.com/firebolt/entertainment#/definitions/EntityInfo" + } + } + }, + "required": [ + "expires", + "totalCount", + "entries" + ], + "additionalProperties": false + }, + "EntityInfoResult": { + "title": "EntityInfoResult", + "description": "The result for an `entityInfo()` push or pull.", + "type": "object", + "properties": { + "expires": { + "type": "string", + "format": "date-time" + }, + "entity": { + "$ref": "https://meta.comcast.com/firebolt/entertainment#/definitions/EntityInfo" + }, + "related": { + "type": "array", + "items": { + "$ref": "https://meta.comcast.com/firebolt/entertainment#/definitions/EntityInfo" + } + } + }, + "required": [ + "expires", + "entity" + ], + "additionalProperties": false + }, + "InterestType": { + "title": "InterestType", + "type": "string", + "enum": [ + "interest", + "disinterest" + ] + }, + "InterestReason": { + "title": "InterestReason", + "type": "string", + "enum": [ + "playlist", + "reaction", + "recording" + ] + } + } + } diff --git a/src/schemas/entertainment.json b/src/schemas/entertainment.json index 8fd14ee38..8f94854e8 100644 --- a/src/schemas/entertainment.json +++ b/src/schemas/entertainment.json @@ -80,7 +80,6 @@ "required": [ "identifiers", "entityType", - "programType", "title" ], "properties": { diff --git a/src/schemas/entity.json b/src/schemas/entity.json new file mode 100644 index 000000000..2e486e865 --- /dev/null +++ b/src/schemas/entity.json @@ -0,0 +1,456 @@ +{ + "$id": "https://meta.comcast.com/firebolt/entity", + "title": "Entity", + "oneOf": [ + { + "$ref": "#/definitions/Entity" + }, + { + "$ref": "#/definitions/PlayableEntity" + }, + { + "$ref": "#/definitions/EntityDetails" + } + ], + "definitions": { + "Entity": { + "oneOf": [ + { + "$ref": "#/definitions/ProgramEntity" + }, + { + "$ref": "#/definitions/MusicEntity" + }, + { + "$ref": "#/definitions/ChannelEntity" + }, + { + "$ref": "#/definitions/UntypedEntity" + }, + { + "$ref": "#/definitions/PlaylistEntity" + } + ] + }, + "EntityDetails": { + "title": "EntityDetails", + "type": "object", + "required": [ + "identifiers" + ], + "properties": { + "identifiers": { + "$ref": "#/definitions/Entity" + }, + "info": { + "$ref": "#/definitions/Metadata" + }, + "waysToWatch": { + "type": "array", + "items": { + "$ref": "https://meta.comcast.com/firebolt/entertainment#/definitions/WayToWatch" + }, + "description": "An array of ways a user is might watch this entity, regardless of entitlements." + } + } + }, + "ChannelEntity": { + "title": "ChannelEntity", + "type": "object", + "properties": { + "entityType": { + "const": "channel" + }, + "channelType": { + "type": "string", + "enum": [ + "streaming", + "overTheAir" + ] + }, + "entityId": { + "type": "string", + "description": "ID of the channel, in the target App's scope." + }, + "appContentData": { + "type": "string", + "maxLength": 256 + } + }, + "required": [ + "entityType", + "channelType", + "entityId" + ], + "additionalProperties": false + }, + "ProgramEntity": { + "title": "ProgramEntity", + "oneOf": [ + { + "$ref": "#/definitions/MovieEntity" + }, + { + "$ref": "#/definitions/TVEpisodeEntity" + }, + { + "$ref": "#/definitions/TVSeasonEntity" + }, + { + "$ref": "#/definitions/TVSeriesEntity" + }, + { + "$ref": "#/definitions/AdditionalEntity" + } + ] + }, + "MusicEntity": { + "title": "MusicEntity", + "type": "object", + "properties": { + "entityType": { + "const": "music" + }, + "musicType": { + "$ref": "https://meta.comcast.com/firebolt/entertainment#/definitions/MusicType" + }, + "entityId": { + "type": "string" + } + }, + "required": [ + "entityType", + "musicType", + "entityId" + ] + }, + "MovieEntity": { + "title": "MovieEntity", + "description": "A Firebolt compliant representation of a Movie entity.", + "type": "object", + "required": [ + "entityType", + "programType", + "entityId" + ], + "properties": { + "entityType": { + "const": "program" + }, + "programType": { + "const": "movie" + }, + "entityId": { + "type": "string" + }, + "assetId": { + "type": "string" + }, + "appContentData": { + "type": "string", + "maxLength": 256 + } + }, + "additionalProperties": false, + "examples": [ + { + "entityType": "program", + "programType": "movie", + "entityId": "el-camino" + } + ] + }, + "TVEpisodeEntity": { + "title": "TVEpisodeEntity", + "description": "A Firebolt compliant representation of a TV Episode entity.", + "type": "object", + "required": [ + "entityType", + "programType", + "entityId", + "seriesId", + "seasonId" + ], + "properties": { + "entityType": { + "const": "program" + }, + "programType": { + "const": "episode" + }, + "entityId": { + "type": "string" + }, + "seriesId": { + "type": "string" + }, + "seasonId": { + "type": "string" + }, + "assetId": { + "type": "string" + }, + "appContentData": { + "type": "string", + "maxLength": 256 + } + }, + "additionalProperties": false, + "examples": [ + { + "entityType": "program", + "programType": "episode", + "entityId": "breaking-bad-pilot", + "seriesId": "breaking-bad", + "seasonId": "breaking-bad-season-1" + } + ] + }, + "TVSeasonEntity": { + "title": "TVSeasonEntity", + "description": "A Firebolt compliant representation of a TV Season entity.", + "type": "object", + "required": [ + "entityType", + "programType", + "entityId", + "seriesId" + ], + "properties": { + "entityType": { + "const": "program" + }, + "programType": { + "const": "season" + }, + "entityId": { + "type": "string" + }, + "seriesId": { + "type": "string" + }, + "assetId": { + "type": "string" + }, + "appContentData": { + "type": "string", + "maxLength": 256 + } + }, + "additionalProperties": false, + "examples": [ + { + "entityType": "program", + "programType": "season", + "entityId": "breaking-bad-season-1", + "seriesId": "breaking-bad" + } + ] + }, + "TVSeriesEntity": { + "title": "TVSeriesEntity", + "description": "A Firebolt compliant representation of a TV Series entity.", + "type": "object", + "required": [ + "entityType", + "programType", + "entityId" + ], + "properties": { + "entityType": { + "const": "program" + }, + "programType": { + "const": "series" + }, + "entityId": { + "type": "string" + }, + "assetId": { + "type": "string" + }, + "appContentData": { + "type": "string", + "maxLength": 256 + } + }, + "additionalProperties": false, + "examples": [ + { + "entityType": "program", + "programType": "series", + "entityId": "breaking-bad" + } + ] + }, + "PlaylistEntity": { + "title": "PlaylistEntity", + "description": "A Firebolt compliant representation of a Playlist entity.", + "type": "object", + "required": [ + "entityType", + "entityId" + ], + "properties": { + "entityType": { + "const": "playlist" + }, + "entityId": { + "type": "string" + }, + "assetId": { + "type": "string" + }, + "appContentData": { + "type": "string", + "maxLength": 256 + } + }, + "additionalProperties": false, + "examples": [ + { + "entityType": "playlist", + "entityId": "playlist/xyz" + } + ] + }, + "PlayableEntity": { + "title": "PlayableEntity", + "anyOf": [ + { + "$ref": "#/definitions/MovieEntity" + }, + { + "$ref": "#/definitions/TVEpisodeEntity" + }, + { + "$ref": "#/definitions/PlaylistEntity" + }, + { + "$ref": "#/definitions/MusicEntity" + }, + { + "$ref": "#/definitions/AdditionalEntity" + } + ] + }, + "AdditionalEntity": { + "title": "AdditionalEntity", + "description": "A Firebolt compliant representation of the remaining program entity types.", + "type": "object", + "required": [ + "entityType", + "programType", + "entityId" + ], + "properties": { + "entityType": { + "const": "program" + }, + "programType": { + "type": "string", + "enum": [ + "concert", + "sportingEvent", + "preview", + "other", + "advertisement", + "musicVideo", + "minisode", + "extra" + ] + }, + "entityId": { + "type": "string" + }, + "assetId": { + "type": "string" + }, + "appContentData": { + "type": "string", + "maxLength": 256 + } + }, + "additionalProperties": false, + "examples": [ + { + "entityType": "program", + "programType": "concert", + "entityId": "live-aid" + } + ] + }, + "UntypedEntity": { + "title": "UntypedEntity", + "allOf": [ + { + "description": "A Firebolt compliant representation of the remaining entity types.", + "type": "object", + "required": [ + "entityId" + ], + "properties": { + "entityId": { + "type": "string" + }, + "assetId": { + "type": "string" + }, + "appContentData": { + "type": "string", + "maxLength": 256 + } + }, + "additionalProperties": false + } + ], + "examples": [ + { + "entityId": "an-entity" + } + ] + }, + "Metadata": { + "title": "Metadata", + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "Title of the entity." + }, + "synopsis": { + "type": "string", + "description": "Short description of the entity." + }, + "seasonNumber": { + "type": "number", + "description": "For TV seasons, the season number. For TV episodes, the season that the episode belongs to." + }, + "seasonCount": { + "type": "number", + "description": "For TV series, seasons, and episodes, the total number of seasons." + }, + "episodeNumber": { + "type": "number", + "description": "For TV episodes, the episode number." + }, + "episodeCount": { + "type": "number", + "description": "For TV seasons and episodes, the total number of episodes in the current season." + }, + "releaseDate": { + "type": "string", + "format": "date-time", + "description": "The date that the program or entity was released or first aired." + }, + "contentRatings": { + "type": "array", + "items": { + "$ref": "https://meta.comcast.com/firebolt/entertainment#/definitions/ContentRating" + }, + "description": "A list of ContentRating objects, describing the entity's ratings in various rating schemes." + } + } + } + } +} \ No newline at end of file diff --git a/src/schemas/intents.json b/src/schemas/intents.json index 6fa2a5807..8f5a86c38 100644 --- a/src/schemas/intents.json +++ b/src/schemas/intents.json @@ -243,12 +243,33 @@ { "$ref": "#/definitions/ButtonIntent" }, + { + "$ref": "#/definitions/FocusIntent" + }, + { + "$ref": "#/definitions/SelectIntent" + }, + { + "$ref": "#/definitions/BackIntent" + }, + { + "$ref": "#/definitions/ExitIntent" + }, + { + "$ref": "#/definitions/ChannelIntent" + }, + { + "$ref": "#/definitions/ScrollIntent" + }, { "$ref": "#/definitions/PowerIntent" }, { "$ref": "#/definitions/VolumeIntent" }, + { + "$ref": "#/definitions/MuteIntent" + }, { "$ref": "#/definitions/MicrophoneIntent" }, @@ -257,15 +278,36 @@ }, { "$ref": "#/definitions/TuneIntent" + }, + { + "$ref": "#/definitions/VoiceGuidanceIntent" + }, + { + "$ref": "#/definitions/HighContrastIntent" + }, + { + "$ref": "#/definitions/ScreenMagnificationIntent" } ] }, "PlaybackControlIntent": { "description": "A Firebolt compliant representation of a user intention to control some aspect of in-progress playback.", "anyOf": [ + { + "$ref": "#/definitions/PlayIntent" + }, { "$ref": "#/definitions/PauseIntent" }, + { + "$ref": "#/definitions/ReplayIntent" + }, + { + "$ref": "#/definitions/StopIntent" + }, + { + "$ref": "#/definitions/PlaybackSpeedIntent" + }, { "$ref": "#/definitions/SeekIntent" }, @@ -273,13 +315,16 @@ "$ref": "#/definitions/SkipIntent" }, { - "$ref": "#/definitions/TrickPlayIntent" + "$ref": "#/definitions/FastForwardIntent" + }, + { + "$ref": "#/definitions/RewindIntent" }, { "$ref": "#/definitions/ClosedCaptionsIntent" }, { - "$ref": "#/definitions/AudioDescriptionIntent" + "$ref": "#/definitions/AudioDescriptionsIntent" } ] }, @@ -307,6 +352,9 @@ "action": { "const": "launch" } + }, + "not": { + "required": [ "data" ] } } ], @@ -338,6 +386,9 @@ "action": { "const": "home" } + }, + "not": { + "required": [ "data" ] } } ], @@ -370,32 +421,7 @@ "const": "entity" }, "data": { - "anyOf": [ - { - "$ref": "#/definitions/MovieEntity" - }, - { - "$ref": "#/definitions/TVEpisodeEntity" - }, - { - "$ref": "#/definitions/TVSeriesEntity" - }, - { - "$ref": "#/definitions/TVSeasonEntity" - }, - { - "$ref": "#/definitions/MusicEntity" - }, - { - "$ref": "#/definitions/PlaylistEntity" - }, - { - "$ref": "#/definitions/AdditionalEntity" - }, - { - "$ref": "#/definitions/UntypedEntity" - } - ] + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/Entity" } } } @@ -414,395 +440,31 @@ } ] }, - "ChannelEntity": { - "title": "ChannelEntity", - "type": "object", - "properties": { - "entityType": { - "const": "channel" - }, - "channelType": { - "type": "string", - "enum": [ - "streaming", - "overTheAir" - ] - }, - "entityId": { - "type": "string", - "description": "ID of the channel, in the target App's scope." - }, - "appContentData": { - "type": "string", - "maxLength": 256 - } - }, - "required": [ - "entityType", - "channelType", - "entityId" - ], - "additionalProperties": false - }, - "ProgramEntity": { - "title": "ProgramEntity", - "type": "object", - "properties": { - "entityType": { - "const": "program" - }, - "programType": { - "$ref": "https://meta.comcast.com/firebolt/entertainment#/definitions/ProgramType" - }, - "entityId": { - "type": "string" - } - }, - "required": [ - "entityType", - "programType", - "entityId" - ] - }, - "MusicEntity": { - "title": "MusicEntity", - "type": "object", - "properties": { - "entityType": { - "const": "music" - }, - "musicType": { - "$ref": "https://meta.comcast.com/firebolt/entertainment#/definitions/MusicType" - }, - "entityId": { - "type": "string" - } - }, - "required": [ - "entityType", - "musicType", - "entityId" - ] - }, - "MovieEntity": { - "title": "MovieEntity", - "allOf": [ - { - "$ref": "#/definitions/ProgramEntity" - }, - { - "description": "A Firebolt compliant representation of a Movie entity.", - "title": "MovieEntity", - "type": "object", - "required": [ - "entityType", - "programType", - "entityId" - ], - "properties": { - "entityType": { - "const": "program" - }, - "programType": { - "const": "movie" - }, - "entityId": { - "$ref": "#/definitions/Identifier" - }, - "assetId": { - "$ref": "#/definitions/Identifier" - }, - "appContentData": { - "type": "string", - "maxLength": 256 - } - }, - "additionalProperties": false - } - ], - "examples": [ - { - "entityType": "program", - "programType": "movie", - "entityId": "el-camino" - } - ] - }, - "TVEpisodeEntity": { - "title": "TVEpisodeEntity", - "allOf": [ - { - "$ref": "#/definitions/ProgramEntity" - }, - { - "description": "A Firebolt compliant representation of a TV Episode entity.", - "title": "TVEpisodeEntity", - "type": "object", - "required": [ - "entityType", - "programType", - "entityId", - "seriesId", - "seasonId" - ], - "properties": { - "entityType": { - "const": "program" - }, - "programType": { - "const": "episode" - }, - "entityId": { - "$ref": "#/definitions/Identifier" - }, - "seriesId": { - "$ref": "#/definitions/Identifier" - }, - "seasonId": { - "$ref": "#/definitions/Identifier" - }, - "assetId": { - "$ref": "#/definitions/Identifier" - }, - "appContentData": { - "type": "string", - "maxLength": 256 - } - }, - "additionalProperties": false - } - ], - "examples": [ - { - "entityType": "program", - "programType": "episode", - "entityId": "breaking-bad-pilot", - "seriesId": "breaking-bad", - "seasonId": "breaking-bad-season-1" - } - ] - }, - "TVSeasonEntity": { - "title": "TVSeasonEntity", - "description": "A Firebolt compliant representation of a TV Season entity.", - "allOf": [ - { - "$ref": "#/definitions/ProgramEntity" - }, - { - "type": "object", - "required": [ - "entityType", - "programType", - "entityId", - "seriesId" - ], - "properties": { - "entityType": { - "const": "program" - }, - "programType": { - "const": "season" - }, - "entityId": { - "$ref": "#/definitions/Identifier" - }, - "seriesId": { - "$ref": "#/definitions/Identifier" - }, - "assetId": { - "$ref": "#/definitions/Identifier" - }, - "appContentData": { - "type": "string", - "maxLength": 256 - } - }, - "additionalProperties": false - } - ], - "examples": [ - { - "entityType": "program", - "programType": "season", - "entityId": "breaking-bad-season-1", - "seriesId": "breaking-bad" - } - ] - }, - "TVSeriesEntity": { - "title": "TVSeriesEntity", + "ChannelIntent": { + "description": "A Firebolt compliant representation of a user intent to 'surf' to the next or previous channel.", + "title": "ChannelIntent", "allOf": [ { - "$ref": "#/definitions/ProgramEntity" - }, - { - "description": "A Firebolt compliant representation of a TV Series entity.", - "type": "object", - "required": [ - "entityType", - "programType", - "entityId" - ], - "properties": { - "entityType": { - "const": "program" - }, - "programType": { - "const": "series" - }, - "entityId": { - "$ref": "#/definitions/Identifier" - }, - "assetId": { - "$ref": "#/definitions/Identifier" - }, - "appContentData": { - "type": "string", - "maxLength": 256 - } - }, - "additionalProperties": false - } - ], - "examples": [ - { - "entityType": "program", - "programType": "series", - "entityId": "breaking-bad" - } - ] - }, - "PlaylistEntity": { - "title": "PlaylistEntity", - "description": "A Firebolt compliant representation of a Playlist entity.", - "type": "object", - "required": [ - "entityType", - "entityId" - ], - "properties": { - "entityType": { - "const": "playlist" - }, - "entityId": { - "$ref": "#/definitions/Identifier" - }, - "assetId": { - "$ref": "#/definitions/Identifier" - }, - "appContentData": { - "type": "string", - "maxLength": 256 - } - }, - "additionalProperties": false, - "examples": [ - { - "entityType": "playlist", - "entityId": "playlist/xyz" - } - ] - }, - "PlayableEntity": { - "title": "PlayableEntity", - "anyOf": [ - { - "$ref": "#/definitions/MovieEntity" - }, - { - "$ref": "#/definitions/TVEpisodeEntity" - }, - { - "$ref": "#/definitions/PlaylistEntity" - }, - { - "$ref": "#/definitions/MusicEntity" + "$ref": "#/definitions/Intent" }, { - "$ref": "#/definitions/AdditionalEntity" - } - ] - }, - "AdditionalEntity": { - "title": "AdditionalEntity", - "allOf": [ - { - "$ref": "#/definitions/ProgramEntity" + "$ref": "#/definitions/IntentProperties" }, { - "description": "A Firebolt compliant representation of the remaining entity types.", "type": "object", - "required": [ - "entityType", - "entityId" - ], + "required": [ "data" ], "properties": { - "entityType": { - "const": "program" + "action": { + "const": "channel" }, - "programType": { + "data": { "type": "string", "enum": [ - "concert", - "sportingEvent", - "preview", - "other", - "advertisement", - "musicVideo", - "minisode", - "extra" + "next", + "previous" ] - }, - "entityId": { - "$ref": "#/definitions/Identifier" - }, - "assetId": { - "$ref": "#/definitions/Identifier" - }, - "appContentData": { - "type": "string", - "maxLength": 256 - } - }, - "additionalProperties": false - } - ], - "examples": [ - { - "entityType": "program", - "programType": "concert", - "entityId": "live-aid" - } - ] - }, - "UntypedEntity": { - "title": "UntypedEntity", - "allOf": [ - { - "description": "A Firebolt compliant representation of the remaining entity types.", - "type": "object", - "required": [ - "entityId" - ], - "properties": { - "entityId": { - "$ref": "#/definitions/Identifier" - }, - "assetId": { - "$ref": "#/definitions/Identifier" - }, - "appContentData": { - "type": "string", - "maxLength": 256 } - }, - "additionalProperties": false - } - ], - "examples": [ - { - "entityId": "an-entity" + } } ] }, @@ -833,7 +495,7 @@ "additionalProperties": false, "properties": { "entity": { - "$ref": "#/definitions/ChannelEntity" + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/ChannelEntity" }, "options": { "description": "The options property of the data property MUST have only one of the following fields.", @@ -902,7 +564,7 @@ "const": "playback" }, "data": { - "$ref": "#/definitions/PlayableEntity" + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/PlayableEntity" } } } @@ -994,7 +656,8 @@ }, "additionalProperties": false } - } + }, + "required": [ "data" ] } ], "examples": [ @@ -1032,7 +695,7 @@ "type": "object", "properties": { "entity": { - "$ref": "#/definitions/PlayableEntity" + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/PlayableEntity" }, "options": { "type": "object", @@ -1076,6 +739,7 @@ "type": "object", "properties": { "options": { + "type": "object", "maxProperties": 1 } } @@ -1084,6 +748,7 @@ "type": "object", "properties": { "options": { + "type": "object", "maxProperties": 0 } } @@ -1272,7 +937,7 @@ "menus": { "type": "array", "items": { - "$ref": "#/definitions/Identifier" + "type": "string" }, "minItems": 1, "maxItems": 100 @@ -1283,7 +948,7 @@ "type": "object", "properties": { "appId": { - "$ref": "#/definitions/Identifier" + "type": "string" }, "exclude": { "type": "boolean" @@ -1369,19 +1034,19 @@ "entity": { "anyOf": [ { - "$ref": "#/definitions/MovieEntity" + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/MovieEntity" }, { - "$ref": "#/definitions/TVEpisodeEntity" + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/TVEpisodeEntity" }, { - "$ref": "#/definitions/TVSeriesEntity" + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/TVSeriesEntity" }, { - "$ref": "#/definitions/TVSeasonEntity" + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/TVSeasonEntity" }, { - "$ref": "#/definitions/AdditionalEntity" + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/AdditionalEntity" } ] }, @@ -1391,24 +1056,24 @@ "type": "object", "properties": { "appId": { - "$ref": "#/definitions/Identifier" + "type": "string" }, "entity": { "anyOf": [ { - "$ref": "#/definitions/MovieEntity" + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/MovieEntity" }, { - "$ref": "#/definitions/TVEpisodeEntity" + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/TVEpisodeEntity" }, { - "$ref": "#/definitions/TVSeriesEntity" + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/TVSeriesEntity" }, { - "$ref": "#/definitions/TVSeasonEntity" + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/TVSeasonEntity" }, { - "$ref": "#/definitions/AdditionalEntity" + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/AdditionalEntity" } ] } @@ -1419,7 +1084,7 @@ "menus": { "type": "array", "items": { - "$ref": "#/definitions/Identifier" + "type": "string" }, "minItems": 1, "maxItems": 100 @@ -1430,7 +1095,7 @@ "type": "object", "properties": { "appId": { - "$ref": "#/definitions/Identifier" + "type": "string" }, "exclude": { "type": "boolean" @@ -1487,9 +1152,9 @@ } ] }, - "ButtonIntent": { - "description": "A Firebolt compliant representation of a user intention to interact with their device in a way analogous to pressing one of the remote buttons.", - "title": "ButtonIntent", + "FocusIntent": { + "description": "A Firebolt compliant representation of a user intention to interact with their device in a way analogous to pressing remote directional pad buttons.", + "title": "FocusIntent", "allOf": [ { "$ref": "#/definitions/Intent" @@ -1499,30 +1164,24 @@ }, { "type": "object", + "required": [ "data" ], "properties": { "action": { - "const": "button" + "const": "focus" }, "data": { "type": "object", "required": [ - "operation" + "direction" ], "properties": { - "operation": { + "direction": { "type": "string", "enum": [ - "down", "up", - "prev", - "next", - "enter", - "exit", - "info", - "menu", - "back", - "cancel", - "record" + "down", + "left", + "right" ] } }, @@ -1533,9 +1192,9 @@ ], "examples": [ { - "action": "button", + "action": "focus", "data": { - "operation": "menu" + "direction": "up" }, "context": { "source": "voice" @@ -1543,9 +1202,9 @@ } ] }, - "VolumeIntent": { - "description": "A Firebolt compliant representation of a user intention to change the device volume.", - "title": "VolumeIntent", + "SelectIntent": { + "description": "A Firebolt compliant representation of a user intention to interact with their device in a way analogous to pressing the remote 'select' button.", + "title": "SelectIntent", "allOf": [ { "$ref": "#/definitions/Intent" @@ -1557,79 +1216,88 @@ "type": "object", "properties": { "action": { - "const": "volume" - }, - "data": { - "anyOf": [ - { - "type": "object", - "properties": { - "value": { - "type": "number", - "minimum": 0, - "maximum": 1 - }, - "toggle": { - "const": true - } - }, - "minProperties": 1, - "maxProperties": 1, - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "value": { - "type": "number", - "minimum": 0, - "maximum": 1 - }, - "relative": { - "const": true - } - }, - "additionalProperties": false - } - ] + "const": "select" } + }, + "not": { + "required": [ "data" ] } } ], "examples": [ { - "action": "volume", - "data": { - "toggle": true - }, + "action": "select", "context": { "source": "voice" } + } + ] + }, + "BackIntent": { + "description": "A Firebolt compliant representation of a user intention to interact with their device in a way analogous to pressing the remote 'back' button.", + "title": "BackIntent", + "allOf": [ + { + "$ref": "#/definitions/Intent" }, { - "action": "volume", - "data": { - "value": 0.7 + "$ref": "#/definitions/IntentProperties" + }, + { + "type": "object", + "properties": { + "action": { + "const": "back" + } }, + "not": { + "required": [ "data" ] + } + } + ], + "examples": [ + { + "action": "back", "context": { "source": "voice" } + } + ] + }, + "ExitIntent": { + "description": "A Firebolt compliant representation of a user intention to interact with their device in a way analogous to pressing the remote 'back' button.", + "title": "ExitIntent", + "allOf": [ + { + "$ref": "#/definitions/Intent" }, { - "action": "volume", - "data": { - "value": 0.1, - "relative": true + "$ref": "#/definitions/IntentProperties" + }, + { + "type": "object", + "properties": { + "action": { + "const": "exit" + } }, + "not": { + "required": [ "data" ] + } + } + ], + "examples": [ + { + "action": "exit", "context": { "source": "voice" } } ] - }, - "PowerIntent": { - "description": "A Firebolt compliant representation of a user intention to turn their device on or off.", - "title": "PowerIntent", + }, + "ScrollIntent": { + "description": "A Firebolt compliant representation of a user intention to interact with their device in a way analogous to pressing remote directional pad buttons.", + "title": "ScrollIntent", "allOf": [ { "$ref": "#/definitions/Intent" @@ -1639,30 +1307,101 @@ }, { "type": "object", + "required": [ "data" ], "properties": { "action": { - "const": "power" + "const": "scroll" }, "data": { - "$ref": "#/definitions/BooleanToggle" + "type": "object", + "required": [ + "direction" + ], + "properties": { + "direction": { + "type": "string", + "enum": [ + "up", + "down", + "left", + "right" + ] + }, + "unit": { + "type": "string", + "enum": [ + "line", + "page", + "percent" + ] + } + }, + "additionalProperties": false } } } ], "examples": [ { - "action": "power", + "action": "scroll", "data": { - "value": false + "direction": "up" }, "context": { "source": "voice" } + } + ] + }, + "ButtonIntent": { + "description": "A Firebolt compliant representation of a user intention to interact with their device in a way analogous to pressing one of the remote buttons.", + "title": "ButtonIntent", + "allOf": [ + { + "$ref": "#/definitions/Intent" }, { - "action": "power", + "$ref": "#/definitions/IntentProperties" + }, + { + "type": "object", + "properties": { + "action": { + "const": "button" + }, + "data": { + "type": "object", + "required": [ + "operation" + ], + "properties": { + "operation": { + "type": "string", + "enum": [ + "down", + "up", + "prev", + "next", + "enter", + "exit", + "info", + "menu", + "back", + "cancel", + "record" + ] + } + }, + "additionalProperties": false + } + } + } + ], + "examples": [ + { + "action": "button", "data": { - "toggle": true + "operation": "menu" }, "context": { "source": "voice" @@ -1670,9 +1409,9 @@ } ] }, - "MicrophoneIntent": { - "description": "A Firebolt compliant representation of a user intention to turn their microphone on or off.", - "title": "MicrophoneIntent", + "VolumeIntent": { + "description": "A Firebolt compliant representation of a user intention to change the device volume.", + "title": "VolumeIntent", "allOf": [ { "$ref": "#/definitions/Intent" @@ -1684,28 +1423,60 @@ "type": "object", "properties": { "action": { - "const": "microphone" + "const": "volume" }, "data": { - "$ref": "#/definitions/BooleanToggle" + "type": "object", + "properties": { + "value": { + "type": "number" + }, + "relative": { + "const": true + } + }, + "required": [ "value" ], + "if": { + "required": [ "relative" ] + }, + "then": { + "properties": { + "value": { + "type": "number", + "minimum": -50, + "maximum": 50 + } + } + }, + "else": { + "properties": { + "value": { + "type": "number", + "minimum": 0, + "maximum": 100 + } + } + }, + "additionalProperties": false } } } ], "examples": [ { - "action": "microphone", + "action": "volume", "data": { - "value": false + "value": 70 }, "context": { "source": "voice" } }, { - "action": "microphone", + "action": "volume", "data": { - "toggle": true + "value": 10, + "relative": true }, "context": { "source": "voice" @@ -1713,9 +1484,9 @@ } ] }, - "InputIntent": { - "description": "A Firebolt compliant representation of a user intention to change which video input is active.", - "title": "InputIntent", + "MuteIntent": { + "description": "A Firebolt compliant representation of a user intention to mute or unmute the device.", + "title": "MuteIntent", "allOf": [ { "$ref": "#/definitions/Intent" @@ -1727,49 +1498,28 @@ "type": "object", "properties": { "action": { - "const": "input" + "const": "mute" }, "data": { - "type": "object", - "required": [ - "interface" - ], - "properties": { - "interface": { - "type": "string", - "enum": [ - "hdmi", - "rca", - "vga", - "etc..." - ] - }, - "number": { - "type": "integer", - "minimum": 1, - "maximum": 100 - } - }, - "additionalProperties": false + "$ref": "#/definitions/BooleanToggle" } } } ], "examples": [ { - "action": "input", + "action": "mute", "data": { - "interface": "hdmi" + "value": false }, "context": { "source": "voice" } }, { - "action": "input", + "action": "mute", "data": { - "interface": "hdmi", - "number": 1 + "toggle": true }, "context": { "source": "voice" @@ -1777,9 +1527,9 @@ } ] }, - "PauseIntent": { - "description": "A Firebolt compliant representation of a user intention to pause/unpause in-progress playback.", - "title": "PauseIntent", + "PowerIntent": { + "description": "A Firebolt compliant representation of a user intention to turn their device on or off.", + "title": "PowerIntent", "allOf": [ { "$ref": "#/definitions/Intent" @@ -1791,28 +1541,529 @@ "type": "object", "properties": { "action": { - "const": "pause" + "const": "power" }, "data": { - "$ref": "#/definitions/BooleanToggle" + "type": "object", + "properties": { + "value": { + "type": "boolean" + }, + "toggle": { + "const": true + }, + "delay": { + "type": "integer", + "minimum": 0 + } + }, + "if": { + "required": [ "value" ] + }, + "then": { + "not": { + "required": [ "toggle" ] + } + }, + "else": { + "required": [ "toggle" ] + }, + "additionalProperties": false + } + } + } + ], + "examples": [ + { + "action": "power", + "data": { + "value": false + }, + "context": { + "source": "voice" + } + }, + { + "action": "power", + "data": { + "toggle": true + }, + "context": { + "source": "voice" + } + }, + { + "action": "power", + "data": { + "value": false, + "delay": 900 + }, + "context": { + "source": "voice" + } + } + ] + }, + "MicrophoneIntent": { + "description": "A Firebolt compliant representation of a user intention to turn their microphone on or off.", + "title": "MicrophoneIntent", + "allOf": [ + { + "$ref": "#/definitions/Intent" + }, + { + "$ref": "#/definitions/IntentProperties" + }, + { + "type": "object", + "properties": { + "action": { + "const": "microphone" + }, + "data": { + "$ref": "#/definitions/BooleanToggle" + } + } + } + ], + "examples": [ + { + "action": "microphone", + "data": { + "value": false + }, + "context": { + "source": "voice" + } + }, + { + "action": "microphone", + "data": { + "toggle": true + }, + "context": { + "source": "voice" + } + } + ] + }, + "InputIntent": { + "description": "A Firebolt compliant representation of a user intention to change which video input is active.", + "title": "InputIntent", + "allOf": [ + { + "$ref": "#/definitions/Intent" + }, + { + "$ref": "#/definitions/IntentProperties" + }, + { + "type": "object", + "properties": { + "action": { + "const": "input" + }, + "data": { + "type": "object", + "required": [ + "interface" + ], + "properties": { + "interface": { + "type": "string", + "enum": [ + "hdmi" + ] + }, + "number": { + "type": "integer", + "minimum": 1, + "maximum": 100 + } + }, + "additionalProperties": false + } + } + } + ], + "examples": [ + { + "action": "input", + "data": { + "interface": "hdmi" + }, + "context": { + "source": "voice" + } + }, + { + "action": "input", + "data": { + "interface": "hdmi", + "number": 1 + }, + "context": { + "source": "voice" + } + } + ] + }, + "PauseIntent": { + "description": "A Firebolt compliant representation of a user intention to pause in-progress playback.", + "title": "PauseIntent", + "allOf": [ + { + "$ref": "#/definitions/Intent" + }, + { + "$ref": "#/definitions/IntentProperties" + }, + { + "type": "object", + "properties": { + "action": { + "const": "pause" + } + } + } + ], + "examples": [ + { + "action": "pause", + "context": { + "source": "voice" + } + } + ] + }, + "PlayIntent": { + "description": "A Firebolt compliant representation of a user intention to play/resume content.", + "title": "PlayIntent", + "allOf": [ + { + "$ref": "#/definitions/Intent" + }, + { + "$ref": "#/definitions/IntentProperties" + }, + { + "type": "object", + "properties": { + "action": { + "const": "play" + } + } + } + ], + "examples": [ + { + "action": "play", + "context": { + "source": "voice" + } + } + ] + }, + "ReplayIntent": { + "description": "A Firebolt compliant representation of a user intention to replay content.", + "title": "ReplayIntent", + "allOf": [ + { + "$ref": "#/definitions/Intent" + }, + { + "$ref": "#/definitions/IntentProperties" + }, + { + "type": "object", + "properties": { + "action": { + "const": "replay" + } + } + } + ], + "examples": [ + { + "action": "replay", + "context": { + "source": "voice" + } + } + ] + }, + "StopIntent": { + "description": "A Firebolt compliant representation of a user intention to stop content.", + "title": "StopIntent", + "allOf": [ + { + "$ref": "#/definitions/Intent" + }, + { + "$ref": "#/definitions/IntentProperties" + }, + { + "type": "object", + "properties": { + "action": { + "const": "stop" + } + } + } + ], + "examples": [ + { + "action": "stop", + "context": { + "source": "voice" + } + } + ] + }, + "PlaybackSpeedIntent": { + "description": "A Firebolt compliant representation of a user intention to change the speed of in-progress playback.", + "title": "PlaybackSpeedIntent", + "allOf": [ + { + "$ref": "#/definitions/Intent" + }, + { + "$ref": "#/definitions/IntentProperties" + }, + { + "type": "object", + "properties": { + "action": { + "const": "speed" + }, + "data": { + "type": "object", + "properties": { + "value": { + "type": "number", + "exclusiveMinimum": 0, + "maximum": 4 + }, + "toggle": { + "type": "boolean" + } + }, + "minProperties": 1, + "maxProperties": 1, + "additionalProperties": false + } + } + } + ], + "examples": [ + { + "action": "speed", + "data": { + "value": 2 + }, + "context": { + "source": "voice" + } + }, + { + "action": "speed", + "data": { + "toggle": true + }, + "context": { + "source": "voice" + } + } + ] + }, + "FastForwardIntent": { + "description": "A Firebolt compliant representation of a user intention to fast-forward in-progress playback.", + "title": "FastForwardIntent", + "allOf": [ + { + "$ref": "#/definitions/Intent" + }, + { + "$ref": "#/definitions/IntentProperties" + }, + { + "type": "object", + "properties": { + "action": { + "const": "fast-forward" + }, + "data": { + "type": "object", + "properties": { + "speed": { + "type": "number", + "exclusiveMinimum": 0, + "maximum": 10 + } + } + } + } + } + ], + "examples": [ + { + "action": "fast-forward", + "data": { + "speed": 2 + }, + "context": { + "source": "voice" + } + }, + { + "action": "fast-forward", + "data": { + "speed": 0.5 + }, + "context": { + "source": "voice" + } + }, + { + "action": "fast-forward", + "context": { + "source": "voice" + } + } + ] + }, + "RewindIntent": { + "description": "A Firebolt compliant representation of a user intention to rewind in-progress playback.", + "title": "RewindIntent", + "allOf": [ + { + "$ref": "#/definitions/Intent" + }, + { + "$ref": "#/definitions/IntentProperties" + }, + { + "type": "object", + "properties": { + "action": { + "const": "rewind" + }, + "data": { + "type": "object", + "properties": { + "speed": { + "type": "number", + "exclusiveMinimum": 0, + "maximum": 10 + } + } + } + } + } + ], + "examples": [ + { + "action": "rewind", + "data": { + "speed": 2 + }, + "context": { + "source": "voice" + } + }, + { + "action": "rewind", + "data": { + "speed": 0.5 + }, + "context": { + "source": "voice" + } + }, + { + "action": "rewind", + "context": { + "source": "voice" + } + } + ] + }, + "SeekIntent": { + "description": "A Firebolt compliant representation of a user intention to seek to a different time for in-progress playback.", + "title": "SeekIntent", + "allOf": [ + { + "$ref": "#/definitions/Intent" + }, + { + "$ref": "#/definitions/IntentProperties" + }, + { + "type": "object", + "properties": { + "action": { + "const": "seek" + }, + "data": { + "allOf": [ + { + "$ref": "#/definitions/DirectionalOperation" + }, + { + "type": "object", + "properties": { + "seconds": { + "type": "number" + }, + "relative": { + "const": true + } + }, + "required": [ "seconds" ], + "if": { + "not": { + "required": [ "relative" ] + } + }, + "then": { + "properties": { + "seconds": { + "type": "number", + "minimum": 0, + "maximum": 86400 + } + } + }, + "else": { + "properties": { + "seconds": { + "type": "number", + "minimum": -43200, + "maximum": 43200 + } + } + } + } + ] } } } ], "examples": [ { - "action": "pause", + "action": "seek", "data": { - "value": false + "seconds": 300 }, "context": { "source": "voice" } }, { - "action": "pause", + "action": "seek", "data": { - "toggle": true + "relative": true, + "seconds": -30 }, "context": { "source": "voice" @@ -1820,9 +2071,9 @@ } ] }, - "PlaybackSpeedIntent": { - "description": "A Firebolt compliant representation of a user intention to change the speed of in-progress playback.", - "title": "PlaybackSpeedIntent", + "SkipIntent": { + "description": "A Firebolt compliant representation of a user intention to skip a scene/chapter/ad during in-progress playback.", + "title": "SkipIntent", "allOf": [ { "$ref": "#/definitions/Intent" @@ -1834,41 +2085,50 @@ "type": "object", "properties": { "action": { - "const": "speed" + "const": "skip" }, "data": { - "type": "object", - "properties": { - "value": { - "type": "number", - "exclusiveMinimum": 0, - "maximum": 4 + "allOf": [ + { + "$ref": "#/definitions/DirectionalOperation" }, - "toggle": { - "type": "boolean" + { + "type": "object", + "properties": { + "count": { + "type": "number", + "exclusiveMinimum": 0, + "maximum": 100 + } + }, + "propertyNames": { + "enum": [ + "direction", + "count" + ] + } } - }, - "minProperties": 1, - "maxProperties": 1, - "additionalProperties": false + ] } } } ], "examples": [ { - "action": "speed", + "action": "skip", "data": { - "value": 2 + "direction": "forward", + "count": 1 }, "context": { "source": "voice" } }, { - "action": "speed", + "action": "skip", "data": { - "toggle": true + "direction": "backward", + "count": 1 }, "context": { "source": "voice" @@ -1876,9 +2136,9 @@ } ] }, - "TrickPlayIntent": { - "description": "A Firebolt compliant representation of a user intention to fast-forward or rewind in-progress playback.", - "title": "TrickPlayIntent", + "ClosedCaptionsIntent": { + "description": "A Firebolt compliant representation of a user intention to enable/disable closed captions.", + "title": "ClosedCaptionsIntent", "allOf": [ { "$ref": "#/definitions/Intent" @@ -1890,50 +2150,28 @@ "type": "object", "properties": { "action": { - "const": "trickplay" + "const": "closed-captions" }, "data": { - "allOf": [ - { - "$ref": "#/definitions/DirectionalOperation" - }, - { - "type": "object", - "properties": { - "speed": { - "type": "number", - "exclusiveMinimum": 0, - "maximum": 10 - } - }, - "propertyNames": { - "enum": [ - "direction", - "speed" - ] - } - } - ] + "$ref": "#/definitions/BooleanToggle" } } } ], "examples": [ { - "action": "trickplay", + "action": "closed-captions", "data": { - "direction": "forward", - "speed": 2 + "value": false }, "context": { "source": "voice" } }, { - "action": "trickplay", + "action": "closed-captions", "data": { - "direction": "backward", - "speed": 2 + "toggle": true }, "context": { "source": "voice" @@ -1941,9 +2179,9 @@ } ] }, - "SeekIntent": { - "description": "A Firebolt compliant representation of a user intention to seek to a different time for in-progress playback.", - "title": "SeekIntent", + "VoiceGuidanceIntent": { + "description": "A Firebolt compliant representation of a user intention to enable/disable voice guidance.", + "title": "VoiceGuidanceIntent", "allOf": [ { "$ref": "#/definitions/Intent" @@ -1955,59 +2193,100 @@ "type": "object", "properties": { "action": { - "const": "seek" + "const": "voice-guidance" }, "data": { - "allOf": [ - { - "$ref": "#/definitions/DirectionalOperation" + "type": "object", + "properties": { + "value": { + "type": "boolean" }, - { - "type": "object", + "toggle": { + "const": true + }, + "speed": { + "type": "integer" + }, + "relative": { + "const": true + }, + "verbosity": { + "type": "string", + "enum": [ + "low", + "hight" + ] + } + }, + "if": { + "required": [ "value" ] + }, + "then": { + "not": { + "required": [ "toggle" ] + }, + "if": { + "required": [ "relative" ] + }, + "then": { "properties": { - "seconds": { - "type": "number", - "minimum": 0, - "maximum": 1800 + "speed": { + "type": "integer", + "minimum": -10, + "maximum": 10 + } + } + }, + "else": { + "properties": { + "speed": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 10 } - }, - "propertyNames": { - "enum": [ - "direction", - "seconds" - ] } } - ] + }, + "else": { + "if": { + "required": [ "toggle" ] + }, + "then": { + "not": { + "required": [ "value" ] + } + } + }, + "additionalProperties": false } } } ], "examples": [ { - "action": "seek", + "action": "voice-guidance", "data": { - "seconds": 300 + "value": true, + "verbosity": "low" }, "context": { "source": "voice" } }, { - "action": "seek", + "action": "voice-guidance", "data": { - "direction": "forward", - "seconds": 30 + "speed": -1, + "relative": true }, "context": { "source": "voice" } - }, + }, { - "action": "seek", + "action": "voice-guidance", "data": { - "direction": "backward", - "seconds": 30 + "toggle": true }, "context": { "source": "voice" @@ -2015,9 +2294,9 @@ } ] }, - "SkipIntent": { - "description": "A Firebolt compliant representation of a user intention to skip a scene/chapter/ad during in-progress playback.", - "title": "SkipIntent", + "AudioDescriptionsIntent": { + "description": "A Firebolt compliant representation of a user intention to enable/disable audio descriptions.", + "title": "AudioDescriptionsIntent", "allOf": [ { "$ref": "#/definitions/Intent" @@ -2029,50 +2308,61 @@ "type": "object", "properties": { "action": { - "const": "skip" + "const": "audio-descriptions" }, "data": { - "allOf": [ - { - "$ref": "#/definitions/DirectionalOperation" + "type": "object", + "properties": { + "value": { + "type": "boolean" }, - { - "type": "object", - "properties": { - "count": { - "type": "number", - "exclusiveMinimum": 0, - "maximum": 100 - } - }, - "propertyNames": { - "enum": [ - "direction", - "count" - ] - } + "toggle": { + "const": true + }, + "language": { + "$ref": "https://meta.comcast.com/firebolt/localization#/definitions/ISO639_2Language" } - ] + }, + "if": { + "required": [ "value" ] + }, + "then": { + "not": { + "required": [ "toggle" ] + } + }, + "else": { + "required": [ "toggle" ] + }, + "additionalProperties": false } } } ], "examples": [ { - "action": "skip", + "action": "audio-descriptions", "data": { - "direction": "forward", - "count": 1 + "value": false }, "context": { "source": "voice" } }, { - "action": "skip", + "action": "audio-descriptions", "data": { - "direction": "backward", - "count": 1 + "toggle": true + }, + "context": { + "source": "voice" + } + }, + { + "action": "audio-descriptions", + "data": { + "value": true, + "language": "eng" }, "context": { "source": "voice" @@ -2080,9 +2370,9 @@ } ] }, - "ClosedCaptionsIntent": { - "description": "A Firebolt compliant representation of a user intention to enable/disable closed captions.", - "title": "ClosedCaptionsIntent", + "HighContrastIntent": { + "description": "A Firebolt compliant representation of a user intention to enable or disable high contrast mode.", + "title": "HighContrastIntent", "allOf": [ { "$ref": "#/definitions/Intent" @@ -2094,7 +2384,7 @@ "type": "object", "properties": { "action": { - "const": "closedcaptions" + "const": "high-contrast" }, "data": { "$ref": "#/definitions/BooleanToggle" @@ -2104,7 +2394,7 @@ ], "examples": [ { - "action": "closedcaptions", + "action": "high-contrast", "data": { "value": false }, @@ -2113,7 +2403,7 @@ } }, { - "action": "closedcaptions", + "action": "high-contrast", "data": { "toggle": true }, @@ -2122,10 +2412,10 @@ } } ] - }, - "AudioDescriptionIntent": { - "description": "A Firebolt compliant representation of a user intention to enable/disable audio descriptions.", - "title": "AudioDescriptionIntent", + }, + "ScreenMagnificationIntent": { + "description": "A Firebolt compliant representation of a user intention to turn screen magnification on or off.", + "title": "ScreenMagnificationIntent", "allOf": [ { "$ref": "#/definitions/Intent" @@ -2137,17 +2427,40 @@ "type": "object", "properties": { "action": { - "const": "audiodescriptions" + "const": "screen-magnification" }, "data": { - "$ref": "#/definitions/BooleanToggle" + "type": "object", + "properties": { + "value": { + "type": "boolean" + }, + "toggle": { + "const": true + }, + "scale": { + "type": "number", + "minimum": 1, + "maximum": 10 + } + }, + "if": { + "required": [ + "toggle" + ] + }, + "then": { + "type": "object", + "maxProperties": 1 + }, + "additionalProperties": false } } } ], "examples": [ { - "action": "audiodescriptions", + "action": "screen-magnification", "data": { "value": false }, @@ -2156,14 +2469,33 @@ } }, { - "action": "audiodescriptions", + "action": "screen-magnification", + "data": { + "value": true, + "scale": 2.5 + }, + "context": { + "source": "voice" + } + }, + { + "action": "screen-magnification", "data": { "toggle": true }, "context": { "source": "voice" } - } + }, + { + "action": "screen-magnification", + "data": { + "value": false + }, + "context": { + "source": "voice" + } + } ] }, "MessageIntent": { @@ -2205,6 +2537,7 @@ } ] }, + "Identifier": { "type": "string" }, @@ -2229,7 +2562,7 @@ "type": "string" }, "appId": { - "$ref": "#/definitions/Identifier" + "type": "string" } } }, diff --git a/src/schemas/lifecycle.json b/src/schemas/lifecycle.json new file mode 100644 index 000000000..906cf41ea --- /dev/null +++ b/src/schemas/lifecycle.json @@ -0,0 +1,38 @@ +{ + "$id": "https://meta.comcast.com/firebolt/lifecycle", + "title": "Lifecycle", + "oneOf": [ + { + "$ref": "#/definitions/LifecycleState" + }, + { + "$ref": "#/definitions/CloseReason" + } + ], + "definitions": { + "LifecycleState": { + "title": "LifecycleState", + "description": "The application lifecycle state", + "type": "string", + "enum": [ + "initializing", + "inactive", + "foreground", + "background", + "unloading", + "suspended" + ] + }, + "CloseReason": { + "title": "CloseReason", + "description": "The application close reason", + "type": "string", + "enum": [ + "remoteButton", + "userExit", + "done", + "error" + ] + } + } +} diff --git a/src/schemas/types.json b/src/schemas/types.json new file mode 100644 index 000000000..0caa49c1f --- /dev/null +++ b/src/schemas/types.json @@ -0,0 +1,194 @@ +{ + "$id": "https://meta.comcast.com/firebolt/types", + "title": "Types", + "definitions": { + "SemanticVersion": { + "title": "SemanticVersion", + "type": "object", + "properties": { + "major": { + "type": "integer", + "minimum": 0 + }, + "minor": { + "type": "integer", + "minimum": 0 + }, + "patch": { + "type": "integer", + "minimum": 0 + }, + "readable": { + "type": "string" + } + }, + "required": [ + "major", + "minor", + "patch", + "readable" + ], + "additionalProperties": false + }, + "AudioProfile": { + "title": "AudioProfile", + "type": "string", + "enum": [ + "stereo", + "dolbyDigital5.1", + "dolbyDigital7.1", + "dolbyDigital5.1+", + "dolbyDigital7.1+", + "dolbyAtmos" + ] + }, + "BooleanMap": { + "title": "BooleanMap", + "type": "object", + "additionalProperties": { + "type": "boolean" + } + }, + "FlatMap": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + } + }, + "LocalizedString": { + "title": "LocalizedString", + "description": "Localized string supports either a simple `string` or a Map of language codes to strings. When using a simple `string`, the current preferred langauge from `Localization.langauge()` is assumed.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + ], + "examples": [ + "A simple string, with no language code", + { + "en": "This is english", + "es": "esto es espaƱol" + } + ] + }, + "ListenResponse": { + "title": "ListenResponse", + "type": "object", + "required": [ + "event", + "listening" + ], + "properties": { + "event": { + "type": "string", + "pattern": "[a-zA-Z]+\\.on[A-Z][a-zA-Z]+" + }, + "listening": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "ProviderRequest": { + "title": "ProviderRequest", + "type": "object", + "required": [ + "correlationId" + ], + "additionalProperties": false, + "properties": { + "correlationId": { + "type": "string", + "description": "The id that was passed in to the event that triggered a provider method to be called" + }, + "parameters": { + "description": "The result of the provider response.", + "type": ["object", "null"] + } + } + }, + "ProviderResponse": { + "title": "ProviderResponse", + "type": "object", + "required": [ + "correlationId" + ], + "additionalProperties": false, + "properties": { + "correlationId": { + "type": "string", + "description": "The id that was passed in to the event that triggered a provider method to be called" + }, + "result": { + "description": "The result of the provider response." + } + } + }, + "Timeout": { + "title": "Timeout", + "description": "Defines the timeout in seconds. If the threshold for timeout is passed for any operation without a result it will throw an error.", + "type": "integer", + "default": 0, + "minimum": 0, + "maximum": 9999 + }, + "Dimensions": { + "type": "object", + "properties": { + "width": { + "type": "integer", + "minimum": 1 + }, + "height": { + "type": "integer", + "minimum": 1 + } + }, + "required": [ "width", "height" ] + }, + "Image": { + "type": "object", + "properties": { + "uri": { + "description": "URI for the image. May be a relative path (e.g. ./foo/image.png) or absolute (e.g. https://foo.com/bar.png) depending on usage.", + "type": "string" + }, + "aspectRatio": { + "description": "The aspect ratio of the image", + "type": "string", + "pattern": "^\\d+x\\d+" + }, + "description": { + "description": "Description of the image.", + "type": "string" + }, + "type": { + "description": "The type of the image.", + "type": "string", + "enum": [ + "icon", "poster", "banner", "splash", "hero" + ] + } + }, + "required": [ + "uri", "aspectRatio", "type" + ] + } + } +} \ No newline at end of file diff --git a/src/sdks/core/package.json b/src/sdks/core/package.json index b97be4a3f..13c9535b3 100644 --- a/src/sdks/core/package.json +++ b/src/sdks/core/package.json @@ -1,6 +1,6 @@ { "name": "@firebolt-js/sdk", - "version": "1.2.0-next.2", + "version": "1.2.0-next.4", "description": "The Firebolt JS SDK", "main": "./dist/lib/firebolt.mjs", "types": "./dist/lib/firebolt.d.ts", diff --git a/src/sdks/core/sdk.config.json b/src/sdks/core/sdk.config.json index f406f8bea..14ba4d55c 100644 --- a/src/sdks/core/sdk.config.json +++ b/src/sdks/core/sdk.config.json @@ -69,7 +69,8 @@ ], "provide": [ "xrn:firebolt:capability:discovery:entity-info", - "xrn:firebolt:capability:discovery:purchased-content" + "xrn:firebolt:capability:discovery:purchased-content", + "xrn:firebolt:capability:discovery:interest" ] }, { diff --git a/src/sdks/core/test/suite/discovery.test.ts b/src/sdks/core/test/suite/discovery.test.ts index 638c57432..7402dce42 100644 --- a/src/sdks/core/test/suite/discovery.test.ts +++ b/src/sdks/core/test/suite/discovery.test.ts @@ -150,3 +150,12 @@ test("clear()", () => { const result: boolean = Discovery.clear(-1000); expect(result).toBeFalsy(); }); + +test("details() provider", () => { + + class myUserInterestProvider implements Discovery.UserInterestProvider { + userInterest(parameters?: object, session?: Discovery.ProviderSession): Promise { + return null + } + } +}) \ No newline at end of file diff --git a/src/sdks/discovery/.npmignore b/src/sdks/discovery/.npmignore new file mode 100644 index 000000000..0e2ba4d22 --- /dev/null +++ b/src/sdks/discovery/.npmignore @@ -0,0 +1,6 @@ +build/* +src/* +test/* +.DS_Store +jest.config.* +tsconfig.* \ No newline at end of file diff --git a/src/sdks/discovery/CHANGELOG.md b/src/sdks/discovery/CHANGELOG.md new file mode 100644 index 000000000..e69de29bb diff --git a/src/sdks/discovery/CONTRIBUTING.md b/src/sdks/discovery/CONTRIBUTING.md new file mode 120000 index 000000000..c97564d93 --- /dev/null +++ b/src/sdks/discovery/CONTRIBUTING.md @@ -0,0 +1 @@ +../../../CONTRIBUTING.md \ No newline at end of file diff --git a/src/sdks/discovery/LICENSE b/src/sdks/discovery/LICENSE new file mode 120000 index 000000000..5853aaea5 --- /dev/null +++ b/src/sdks/discovery/LICENSE @@ -0,0 +1 @@ +../../../LICENSE \ No newline at end of file diff --git a/src/sdks/discovery/NOTICE b/src/sdks/discovery/NOTICE new file mode 120000 index 000000000..295f6bdb3 --- /dev/null +++ b/src/sdks/discovery/NOTICE @@ -0,0 +1 @@ +../../../NOTICE \ No newline at end of file diff --git a/src/sdks/discovery/README.md b/src/sdks/discovery/README.md new file mode 100644 index 000000000..2ec46b95d --- /dev/null +++ b/src/sdks/discovery/README.md @@ -0,0 +1,26 @@ +--- +title: Firebolt Discovery SDK +--- + +[![semantic-release: conventional](https://img.shields.io/badge/semantic--release-conventional-e10079?logo=semantic-release)](https://github.com/semantic-release/semantic-release) + +# Firebolt Discovery SDK +For building Firebolt compliant apps for discovering first-party content on Firebolt devices. + +## Usage +To install, run: + +``` +npm install @firebolt-js/discovery-sdk +``` + +To use the package, import one of it's modules, e.g.: + +```js +import { Content } from '@firebolt-js/discovery-sdk' +``` + +## Contributing +The Firebolt SDKs are built using the Firebolt OpenRPC toolset: + +See [Firebolt OpenRPC](https://www.github.com/rdkcentral/firebolt-openrpc/), for more info. diff --git a/src/sdks/discovery/jest.config.json b/src/sdks/discovery/jest.config.json new file mode 100644 index 000000000..745370cfa --- /dev/null +++ b/src/sdks/discovery/jest.config.json @@ -0,0 +1,8 @@ +{ + "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|js?)$", + "transform": {}, + "testPathIgnorePatterns": ["/node_modules/", "/../../../test"], + "moduleFileExtensions": ["js", "jsx", "mjs"], + "verbose": true, + "testEnvironment": "jest-environment-jsdom" + } \ No newline at end of file diff --git a/src/sdks/discovery/package.json b/src/sdks/discovery/package.json new file mode 100644 index 000000000..40e4cd992 --- /dev/null +++ b/src/sdks/discovery/package.json @@ -0,0 +1,53 @@ +{ + "name": "@firebolt-js/discovery-sdk", + "version": "1.2.0-next.2", + "description": "The Firebolt Discovery JS SDK", + "main": "./dist/lib/firebolt-discovery.mjs", + "types": "./dist/lib/firebolt-discovery.d.ts", + "exports": { + ".": "./dist/lib/firebolt-discovery.mjs" + }, + "type": "module", + "scripts": { + "validate": "npx firebolt-openrpc validate --input ./dist/firebolt-discovery-open-rpc.json", + "sdk": "npx firebolt-openrpc sdk --input ./dist/firebolt-discovery-open-rpc.json --template ./src/js --output ./build/javascript/src", + "native": "npx firebolt-openrpc sdk --input ./dist/firebolt-discovery-open-rpc.json --template ./src/js --output ./build/c/src --language ../../../node_modules/@firebolt-js/openrpc/languages/c", + "compile": "cd ../../.. && npm run compile", + "slice": "npx firebolt-openrpc slice -i ../../../dist/firebolt-open-rpc.json --sdk ./sdk.config.json -o ./dist/firebolt-discovery-open-rpc.json", + "docs": "npx firebolt-openrpc docs --input ./dist/firebolt-discovery-open-rpc.json --output build/docs/markdown --as-path", + "wiki": "npx firebolt-openrpc docs --input ./dist/firebolt-discovery-open-rpc.json --output build/docs/markdown", + "dist:notest": "npm run clean && npm run slice && npm run validate && npm run sdk && npm run docs && npm run prettier && npm run dist:copy && echo 'Firebolt Discovery SDK /dist/ is ready.\n'", + "dist:copy": "npm run dist:copy:sdk && npm run dist:copy:docs", + "dist:copy:sdk": "mkdirp ./dist && cp -R build/javascript/src dist/lib && cp ./dist/firebolt-discovery-open-rpc.json ../../../dist/firebolt-discovery-open-rpc.json", + "dist:copy:docs": "mkdirp ./dist && cp -R build/docs/markdown dist/docs", + "dist": "npm run dist:notest && npm run test", + "clean": "rm -rf ./build && rm -rf ./dist", + "test:setup": "rm -rf test/transpiled-suite && npx tsc --target es6 --moduleResolution node --outDir test/transpiled-suite", + "test": "npm run test:setup && NODE_OPTIONS=--experimental-vm-modules npx --config=jest.config.json --detectOpenHandles jest", + "prepack": "node ../../js/version.mjs validate && npm run broilerplate", + "broilerplate": "rm ./CONTRIBUTING.md && cp ../../../CONTRIBUTING.md ./CONTRIBUTING.md && rm ./LICENSE && cp ../../../LICENSE ./LICENSE && rm ./NOTICE && cp ../../../NOTICE ./NOTICE", + "prettier": "prettier build/**/*.mjs --write --parser babel && prettier build/**/*.md --write --parser markdown" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/rdkcentral/firebolt-core-sdk", + "directory": "src/sdks/discovery" + }, + "author": "", + "bugs": { + "url": "https://github.com/rdkcentral/firebolt-core-sdk/issues" + }, + "homepage": "https://github.com/rdkcentral/firebolt-core-sdk#readme", + "devDependencies": { + "jest": "^28.1.0", + "jest-environment-jsdom": "^28.1.3", + "prettier": "^3.1.0", + "typescript": "^4.6.4" + }, + "keywords": [ + "firebolt", + "apps", + "sdk" + ], + "license": "Apache-2.0" +} \ No newline at end of file diff --git a/src/sdks/discovery/sdk.config.json b/src/sdks/discovery/sdk.config.json new file mode 100644 index 000000000..535d3bb12 --- /dev/null +++ b/src/sdks/discovery/sdk.config.json @@ -0,0 +1,15 @@ +{ + "info": { + "title": "Firebolt Discovery SDK" + }, + "methods": [ + { + "module": "Content", + "use": [ + "xrn:firebolt:capability:discovery:purchased-content", + "xrn:firebolt:capability:discovery:entity-info", + "xrn:firebolt:capability:discovery:interest" + ] + } + ] +} diff --git a/src/sdks/discovery/src/js/sdk/index.mjs b/src/sdks/discovery/src/js/sdk/index.mjs new file mode 100644 index 000000000..a9f2f5ca6 --- /dev/null +++ b/src/sdks/discovery/src/js/sdk/index.mjs @@ -0,0 +1,30 @@ +/* + * Copyright 2021 Comcast Cable Communications Management, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { setMockResponses } from './Transport/MockTransport.mjs' + +/* ${MOCK_IMPORTS} */ + +setMockResponses({ + /* ${MOCK_OBJECTS} */ +}) + +/* ${EXPORTS} */ +export { default as Log } from './Log/index.mjs' +export { default as Events } from './Events/index.mjs' +export { default as Settings } from './Settings/index.mjs' diff --git a/src/sdks/discovery/test/suite/content.test.ts b/src/sdks/discovery/test/suite/content.test.ts new file mode 100644 index 000000000..1ab4e98c8 --- /dev/null +++ b/src/sdks/discovery/test/suite/content.test.ts @@ -0,0 +1,43 @@ +/* + * Copyright 2021 Comcast Cable Communications Management, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { test, expect } from "@jest/globals"; +import { Content } from "../../build/javascript/src/firebolt-discovery"; + +test("Content.requestUserInterest()", () => { + return Content.requestUserInterest(Content.InterestType.INTEREST, Content.InterestReason.PLAYLIST).then((interest: Content.InterestResult) => { + const entity = interest.entity + const appId = interest.appId + expect(appId).toBeDefined() + expect(entity).toBeDefined() + expect(entity.info.title).toBe("Cool Runnings") + }) +}); + +test("Content.onUserInterest()", () => { + return Content.listen('userInterest', (interest: Content.InterestEvent) => { + const entity = interest.entity + const appId = interest.appId + const reason = interest.reason + expect(interest['type']).toBeDefined() + expect(reason).toBeDefined() + expect(appId).toBeDefined() + expect(entity).toBeDefined() + expect(entity.info.title).toBe("Cool Runnings") + }) +}); diff --git a/src/sdks/discovery/tsconfig.json b/src/sdks/discovery/tsconfig.json new file mode 100644 index 000000000..56ff58bc8 --- /dev/null +++ b/src/sdks/discovery/tsconfig.json @@ -0,0 +1,5 @@ +{ + "include": [ + "test/suite/*" + ] +} \ No newline at end of file diff --git a/src/sdks/manage/package.json b/src/sdks/manage/package.json index c21e6138e..9f3cbc6d3 100644 --- a/src/sdks/manage/package.json +++ b/src/sdks/manage/package.json @@ -1,6 +1,6 @@ { "name": "@firebolt-js/manage-sdk", - "version": "1.2.0-next.2", + "version": "1.2.0-next.4", "description": "The Firebolt Manage JS SDK", "main": "./dist/lib/firebolt-manage.mjs", "types": "./dist/lib/firebolt-manage.d.ts", diff --git a/src/sdks/manage/test/suite/keyboard.test.ts b/src/sdks/manage/test/suite/keyboard.test.ts index c40b7d40e..6ee3a5aed 100644 --- a/src/sdks/manage/test/suite/keyboard.test.ts +++ b/src/sdks/manage/test/suite/keyboard.test.ts @@ -82,19 +82,19 @@ class DelegatingKBProvider implements Keyboard.KeyboardInputProvider { standard( parameters: Keyboard.KeyboardParameters, session: Keyboard.FocusableProviderSession - ): Promise { + ): Promise { return this.delegate.standard(parameters, session) } password( parameters: Keyboard.KeyboardParameters, session: Keyboard.FocusableProviderSession - ): Promise { + ): Promise { return this.delegate.password(parameters, session) } email( parameters: Keyboard.KeyboardParameters, session: Keyboard.FocusableProviderSession - ): Promise { + ): Promise { return this.delegate.email(parameters, session) } } @@ -103,21 +103,19 @@ class KBProvider implements Keyboard.KeyboardInputProvider { standard( parameters: Keyboard.KeyboardParameters, session: Keyboard.FocusableProviderSession - ): Promise { - return Promise.resolve({ - text: 'foo' - }); + ): Promise { + return Promise.resolve('foo'); } password( parameters: Keyboard.KeyboardParameters, session: Keyboard.FocusableProviderSession - ): Promise { + ): Promise { return Promise.resolve(null); } email( parameters: Keyboard.KeyboardParameters, session: Keyboard.FocusableProviderSession - ): Promise { + ): Promise { return Promise.resolve(null); } } @@ -126,19 +124,19 @@ class KBProviderWithError implements Keyboard.KeyboardInputProvider { async standard( parameters: Keyboard.KeyboardParameters, session: Keyboard.FocusableProviderSession - ): Promise { + ): Promise { throw new Error('failed') } async password( parameters: Keyboard.KeyboardParameters, session: Keyboard.FocusableProviderSession - ): Promise { + ): Promise { throw new Error('failed') } async email( parameters: Keyboard.KeyboardParameters, session: Keyboard.FocusableProviderSession - ): Promise { + ): Promise { throw new Error('failed') } } @@ -156,7 +154,7 @@ test("Keyboard.provide() declarations", async () => { let result = await promise console.log(result) expect(result.method).toStrictEqual('keyboard.standardResponse') - expect(result.params.result.text).toStrictEqual('foo') + expect(result.params.result).toStrictEqual('foo') }); test("Keyboard.provide() with blank object", () => {