@pbvision/partner-sdk 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # PB Vision Partner Documentation <!-- omit in toc -->
2
2
 
3
3
  The easiest way to use the PB Vision AI Engine is to simply share a link to
4
- your video file. Our AI usually works on each video for about an hour for a
4
+ your video file. Our AI usually works on each video for about 30 minutes for a
5
5
  standard length game, so we'll notify your servers when the results are ready.
6
6
 
7
7
  - [Installation](#installation)
@@ -10,7 +10,7 @@ standard length game, so we'll notify your servers when the results are ready.
10
10
  - [Using the API](#using-the-api)
11
11
  - [Webhook Setup](#webhook-setup)
12
12
  - [Send Videos](#send-videos)
13
- - [Option 1: PBV Downloads your video from a URL](#option-1-pbv-downloads-your-video-from-a-url)
13
+ - [Option 1: PBV downloads your video from a URL](#option-1-pbv-downloads-your-video-from-a-url)
14
14
  - [Option 2: Upload your video](#option-2-upload-your-video)
15
15
  - [After Video Processing is Done](#after-video-processing-is-done)
16
16
  - [Video Guidelines](#video-guidelines)
@@ -31,8 +31,8 @@ account. We'll send you an API Key.
31
31
  4. Initialize the SDK:
32
32
 
33
33
  ```javascript
34
- import { PBVision } from '@pbvision/partner-sdk'
35
- const pbv = new PBVision(YOUR_API_KEY)
34
+ import { PBVision } from '@pbvision/partner-sdk';
35
+ const pbv = new PBVision(YOUR_API_KEY);
36
36
  ```
37
37
 
38
38
  5. Use its methods as described to [send us video URLs](#send-videos) to
@@ -47,10 +47,10 @@ an HTTP POST request to a URL ("webhook") of your choice. You can use our SDK
47
47
  to tell our servers where you want us to notify your servers like this:
48
48
 
49
49
  ```javascript
50
- import { PBVision } from '@pbvision/partner-sdk'
50
+ import { PBVision } from '@pbvision/partner-sdk';
51
51
 
52
- const pbv = new PBVision(YOUR_API_KEY)
53
- await pbv.setWebhook(YOUR_WEBHOOK_URL)
52
+ const pbv = new PBVision(YOUR_API_KEY);
53
+ await pbv.setWebhook(YOUR_WEBHOOK_URL);
54
54
  ```
55
55
 
56
56
  Alternatively, you can just use `curl`:
@@ -65,7 +65,7 @@ curl -X POST \
65
65
 
66
66
  ### Send Videos
67
67
 
68
- #### Option 1: PBV Downloads your video from a URL
68
+ #### Option 1: PBV downloads your video from a URL
69
69
 
70
70
  First, upload a video to a _publicly_ accessible URL on your server. For best
71
71
  results, videos should follow our [guidelines](#video-guidelines).
@@ -73,17 +73,17 @@ results, videos should follow our [guidelines](#video-guidelines).
73
73
  Next, tell us to download and work on the video. You can do this using our SDK:
74
74
 
75
75
  ```javascript
76
- import { PBVision } from '@pbvision/partner-sdk'
76
+ import { PBVision } from '@pbvision/partner-sdk';
77
77
 
78
- const pbv = new PBVision(YOUR_API_KEY)
79
- // you can omit this metadata, or provide some or all of this object-- whatever you'd like!
78
+ const pbv = new PBVision(YOUR_API_KEY);
79
+ // you can omit this metadata, or provide some or all of this object—whatever you'd like!
80
80
  const optionalMetadata = {
81
81
  userEmails: [],
82
82
  name: 'Dink Championship 2024',
83
83
  desc: 'A longer description, if you want',
84
- gameStartEpoch: 1711393200 // when the game was played
85
- }
86
- await pbv.sendVideoUrlToDownload(YOUR_VIDEO_URL, optionalMetadata)
84
+ gameStartEpoch: 1711393200, // when the game was played
85
+ };
86
+ await pbv.sendVideoUrlToDownload(YOUR_VIDEO_URL, optionalMetadata);
87
87
  ```
88
88
 
89
89
  Alternatively, you can just use `curl`:
@@ -103,38 +103,41 @@ To use the production server instead of the test server, change `ko3kowqi6a` to
103
103
  You can directly upload your video from a file using the SDK too:
104
104
 
105
105
  ```javascript
106
- import { PBVision } from '@pbvision/partner-sdk'
106
+ import { PBVision } from '@pbvision/partner-sdk';
107
107
 
108
- const pbv = new PBVision(YOUR_API_KEY, { useProdServer: false })
109
- // you can omit this metadata, or provide some or all of this object-- whatever you'd like!
108
+ const pbv = new PBVision(YOUR_API_KEY, { useProdServer: false });
109
+ // you can omit this metadata, or provide some or all of this object—whatever you'd like!
110
110
  const optionalMetadata = {
111
- userEmails: [],
111
+ userEmails: [], // passthrough partners must provide at least one email here
112
112
  name: 'Dink Championship 2024',
113
113
  desc: 'A longer description, if you want',
114
- gameStartEpoch: 1711393200 // when the game was played
115
- }
116
- await pbv.uploadVideo(YOUR_VIDEO_FILENAME, optionalMetadata)
114
+ gameStartEpoch: 1711393200, // when the game was played
115
+ };
116
+ await pbv.uploadVideo(YOUR_VIDEO_FILENAME, optionalMetadata);
117
117
  ```
118
118
 
119
119
  ## After Video Processing is Done
120
120
 
121
121
  When our AI is done processing your video, we will email the players in the
122
- game (_if_ their email addresses were provided).
122
+ game (_if_ their email addresses were provided via `userEmails`).
123
123
 
124
124
  If you [provided a webhook](#webhook-setup), then we'll send
125
125
  an HTTP POST request to your server. Your server should acknowledge this
126
126
  callback with a standard 200 (OK) response. If it does not, we will attempt to
127
127
  retry sending this request later (up to some maximum number of attempts).
128
128
 
129
- The HTTP POST body will contain JSON which looks like this:
129
+ The HTTP POST body will contain JSON which looks like this (depending on which
130
+ data is enabled for your API key, only some of these fields may be present):
130
131
 
131
132
  ```json
132
133
  {
133
134
  "from_url": "https://example.com/my-video.mp4",
134
- "webpage": "https://app.pb.vision/video/xyz",
135
+ "webpage": "https://pb.vision/video/tvgz3pqij0ll",
135
136
  "cutVideo": "https://storage.googleapis.com/pbv-pro/xyz/f50272db-69a8-49ed-9d92-3a4d067af87c/rallies.mp4",
136
- "events": EVENTS_DATA,
137
137
  "insights": INSIGHTS_DATA,
138
+ "stats": STATS_DATA,
139
+ "vid": STRING,
140
+ "aiEngineVersion": INT,
138
141
  "error": {
139
142
  "reason": "some explanation here..."
140
143
  }
@@ -144,30 +147,36 @@ The HTTP POST body will contain JSON which looks like this:
144
147
  - `from_url` is the video url you sent us in [step 3](#send-videos)
145
148
  - `webpage` is a link to our web app where the stats can be explored
146
149
  - `error` is only present if your video could not be processed
147
- - `events` - describes the pickleball game in detailed, low-level events
148
- - Explore the _events_ schema at <https://pbv-public.github.io/events/dev>
149
- - Schema changes and diffs are in our [`pbv-public/events` repo](https://github.com/pbv-public/events/blob/dev/CHANGELOG.md)
150
- - `insights` - a summary of high-level stats about the video
151
- - Explore the _insights_ schema at <https://pbv-public.github.io/insights/dev>
150
+ - `insights` describes the pickleball game in a detailed, shot-by-shot format
151
+ - Explore the _insights_ schema at <https://pbv-public.github.io/insights?s=~insights~game>
152
152
  - Schema changes and diffs are in our [`pbv-public/insights` repo](https://github.com/pbv-public/insights/blob/dev/CHANGELOG.md)
153
+ - `stats` various stats about the game for advanced players
154
+ - Explore the _stats_ schema at <https://pbv-public.github.io/stats?s=~stats~game>
155
+ - Schema changes and diffs are in our [`pbv-public/stats` repo](https://github.com/pbv-public/stats/blob/dev/CHANGELOG.md)
156
+ - `cutVideo` contains a link to the rally-sliced (dead time removed) .mp4.
157
+ - only included if `insights` is included:
158
+ - `vid` - the unique ID of the video in our system
159
+ - `aiEngineVersion` - the version number of our AI used to process the video
160
+ - Using these two values, you can retrieve the player thumbnail images extracted from the video like: `https://storage.googleapis.com/pbv-pro/${vid}/${aiEngineVersion}/player${playerIndex}-${imageIndex}.jpg` where `playerIndex` is in the range [0, 3] (for doubles games) and `imageIndex` is in the range [0, 7].
153
161
 
154
162
  ## Video Guidelines
155
163
 
156
164
  Requirements:
157
165
 
158
- - Video Encoding: **H.264** codec
159
- - Frame Rate: **30 or 60 FPS**
160
- - File Extension: `.mp4`
166
+ - Comply with [PB Vision's framing guidelines](https://help.pb.vision/en/help/articles/1108176-video-recording-and-framing-tips)
167
+ - Video Encoding: H.264 codec
168
+ - Frame Rate: 30 or 60 FPS
169
+ - Max Duration: 30 minutes
161
170
 
162
171
  For best results, we also recommend:
163
172
 
164
- - Follow our [**video framing** guidelines](https://www.pb.vision/video-tips)
173
+ - File Extension: .mp4
165
174
  - Audio Encoding: MPEG-4 AAC
166
175
  - Resolution: 1080p
167
- - Max bitrate: 2 Mbps
168
- - Max file size: 1GB
169
- - Max duration: 30 minutes
176
+ - Frame Rate: 30 FPS
177
+ - Max Bitrate: 2 Mbps
178
+ - Max File Size: 2GB
170
179
 
171
180
  ## Reference Guide
172
181
 
173
- - [JSDocs](https://pbv-public.github.io/partner-sdk-nodejs/)
182
+ - [JSDocs](https://pbv-public.github.io/partner-sdk-nodejs/)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@pbvision/partner-sdk",
3
3
  "description": "SDK for interacting with the PB Vision pickleball AI engine.",
4
- "version": "0.1.3",
4
+ "version": "0.1.5",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "engines": {
package/src/index.js CHANGED
@@ -1,7 +1,6 @@
1
1
  import assert from 'node:assert'
2
2
  import fs from 'node:fs'
3
3
 
4
- import { makeSignedGoogleCloudStorageVideoObjectName } from './video-object-name.js'
5
4
  import fetch from 'node-fetch'
6
5
 
7
6
  const ENVIRONMENTS = {
@@ -26,10 +25,11 @@ const ENVIRONMENTS = {
26
25
  /** @public */
27
26
  export class PBVision {
28
27
  constructor (apiKey, { useProdServer = false } = {}) {
29
- assert(apiKey && apiKey.length === 31 && apiKey.indexOf('_') === 10,
30
- `invalid API key: ${apiKey}`)
28
+ const underscoreIndex = apiKey.lastIndexOf('_')
29
+ assert(apiKey && underscoreIndex !== -1, `invalid API key: ${apiKey}`)
31
30
 
32
31
  this.apiKey = apiKey
32
+ this.uid = apiKey.substring(0, underscoreIndex)
33
33
  const config = useProdServer ? ENVIRONMENTS.prod : ENVIRONMENTS.test
34
34
  this.server = config.apiServer
35
35
  this.isDev = config === ENVIRONMENTS.test
@@ -47,20 +47,29 @@ export class PBVision {
47
47
  return this.__callAPI('webhook/set', { url: webhookUrl })
48
48
  }
49
49
 
50
+ /**
51
+ * @typedef {Object} VideoUrlToDownloadResponse
52
+ * @property {string} vid the ID of the new video
53
+ * @property {boolean} [hasCredits] for passthrough partners, this field will
54
+ * be present and indicate whether the first user has any credits available
55
+ */
56
+
50
57
  /**
51
58
  * Tells PB Vision to download the specified video and process it. When
52
59
  * processing is complete, your webhook URL will receive a callback.
53
60
  *
54
61
  * @param {string} videoUrl the publicly available URL of the video
55
62
  * @param {VideoMetadata} [metadata]
63
+ * @returns {VideoUrlToDownloadResponse}
56
64
  */
57
65
  async sendVideoUrlToDownload (videoUrl, { userEmails = [], name, desc, gameStartEpoch } = {}) {
58
66
  assert(typeof videoUrl === 'string' && videoUrl.startsWith('http'),
59
67
  'URL must be a string beginning with http')
60
68
  assert(videoUrl.split('?')[0].endsWith('.mp4'), 'video URL must have the .mp4 extension')
61
- this.__callAPI(
69
+ const resp = await this.__callAPI(
62
70
  'add_video_by_url',
63
71
  { url: videoUrl, userEmails, name, desc, gameStartEpoch })
72
+ return JSON.parse(resp)
64
73
  }
65
74
 
66
75
  async __callAPI (path, body) {
@@ -95,34 +104,30 @@ export class PBVision {
95
104
 
96
105
  /**
97
106
  * Upload a video for processing by the AI.
107
+ *
108
+ * For passthrough partners, the video is only uploaded if the paying user
109
+ * has credit(s) available with which the video can be analyzed.
110
+ *
98
111
  * @param {string} mp4Filename
99
112
  * @param {VideoMetadata} [metadata]
100
- * @returns {string} the full Google Cloud Storage filename which this was
101
- * uploaded to
113
+ * @returns {VideoUrlToDownloadResponse}
102
114
  */
103
115
  async uploadVideo (mp4Filename, { userEmails = [], name, desc, gameStartEpoch } = {}) {
104
- // get the upload signing key if we didn't get it earlier
105
- if (!this.signingKey) {
106
- this.signingKey = await this.__callAPI('get_signing_key')
116
+ const pieces = mp4Filename.split('.')
117
+ const ext = pieces[pieces.length - 1]
118
+ const makeVIDResp = await this.__callAPI('make_video_id', { userEmails, name, desc, gameStartEpoch, fileExt: ext })
119
+ const { hasCredits, vid } = JSON.parse(makeVIDResp)
120
+ if (hasCredits === false) {
121
+ return { hasCredits }
107
122
  }
108
-
109
- // create a unique, signed upload name
110
- const uid = this.apiKey.split('_')[0]
111
123
  const bucket = `pbv-uploads${this.isDev ? '-dev' : ''}`
112
- const objName = makeSignedGoogleCloudStorageVideoObjectName({
113
- uploaderUID: uid,
114
- signingKey: this.signingKey,
115
- bucket
116
- })
117
-
118
- // if given extra information about this video, upload that first
119
- if (userEmails.length || name || desc || gameStartEpoch) {
120
- const body = { objName, userEmails, name, desc, gameStartEpoch }
121
- await this.__callAPI('set_pre_upload_metadata', body)
122
- }
123
-
124
+ const objName = `${this.uid}/${vid}.${ext}`
124
125
  await uploadToGCS(bucket, objName, mp4Filename)
125
- return objName
126
+ const ret = { vid }
127
+ if (hasCredits !== undefined) {
128
+ ret.hasCredits = hasCredits
129
+ }
130
+ return ret
126
131
  }
127
132
  }
128
133
 
@@ -1,68 +0,0 @@
1
- import assert from 'node:assert'
2
- import crypto from 'node:crypto'
3
-
4
- /**
5
- * Creates the name for a video file to upload to our uploads bucket.
6
- *
7
- * The name is signed with the uploader's API Key to prove that the upload is
8
- * indeed from them.
9
- *
10
- * The object name is formatted like:
11
- * <User ID>/<Video ID>-<Security Hash>.<File Extension>
12
- *
13
- * @param {Object} opts information about the video upload
14
- * @param {string} opts.uploaderUID the video uploader's user ID
15
- * @param {string} opts.signingKey the video uploader's API Key
16
- * @param {string} [opts.fileExt] the video's file extension
17
- * @param {string} [opts.bucket] the GCS bucket to upload to
18
- * @returns {string} the signed GCS object filename
19
- * @protected
20
- */
21
- export function makeSignedGoogleCloudStorageVideoObjectName ({
22
- uploaderUID,
23
- signingKey,
24
- fileExt = 'mp4',
25
- bucket = 'pbv-uploads',
26
- uploadId = undefined
27
- }) {
28
- assert(typeof uploaderUID === 'string' && uploaderUID.length)
29
- assert(typeof signingKey === 'string' && signingKey.length)
30
- uploadId = uploadId ?? crypto.randomUUID().replace(/-/g, '')
31
- assert(typeof uploadId === 'string' && uploadId.length === 32)
32
- return makeObjectName(uploaderUID, signingKey, uploadId, fileExt, bucket)
33
- }
34
-
35
- /**
36
- * Returns the hash from the parameters used to construct a GCS object name.
37
- *
38
- * The hash includes all elements of the file's unique path (including the
39
- * bucket) except for the hash itself.
40
- * @private
41
- */
42
- function getHash (uploaderUID, signingKey, uploadId, fileExt, bucket) {
43
- const nameWithEmptyHash = makeObjectNameGivenHash(
44
- uploaderUID, uploadId, fileExt, '')
45
- return crypto.createHash('sha256')
46
- .update(signingKey + bucket + nameWithEmptyHash)
47
- .digest('hex')
48
- }
49
-
50
- /**
51
- * Returns the full GCS object name (including hash) for the given parameters.
52
- * @private
53
- */
54
- function makeObjectName (uploaderUID, signingKey, uploadId, fileExt, bucket) {
55
- const hash = getHash(uploaderUID, signingKey, uploadId, fileExt, bucket)
56
- const nameWithHash = makeObjectNameGivenHash(
57
- uploaderUID, uploadId, fileExt, hash)
58
- return nameWithHash
59
- }
60
-
61
- /**
62
- * Returns the full GCS object name using the specified hash string.
63
- * @private
64
- */
65
- function makeObjectNameGivenHash (uploaderUID, uploadId, fileExt, hash) {
66
- assert(uploaderUID.indexOf('/') === -1)
67
- return `${uploaderUID}/${uploadId}-${hash}.${fileExt}`
68
- }