@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 +47 -38
- package/package.json +1 -1
- package/src/index.js +30 -25
- package/src/video-object-name.js +0 -68
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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://
|
|
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
|
-
- `
|
|
148
|
-
- Explore the
|
|
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
|
-
-
|
|
159
|
-
-
|
|
160
|
-
-
|
|
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
|
-
-
|
|
173
|
+
- File Extension: .mp4
|
|
165
174
|
- Audio Encoding: MPEG-4 AAC
|
|
166
175
|
- Resolution: 1080p
|
|
167
|
-
-
|
|
168
|
-
- Max
|
|
169
|
-
- Max
|
|
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
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
|
-
|
|
30
|
-
|
|
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 {
|
|
101
|
-
* uploaded to
|
|
113
|
+
* @returns {VideoUrlToDownloadResponse}
|
|
102
114
|
*/
|
|
103
115
|
async uploadVideo (mp4Filename, { userEmails = [], name, desc, gameStartEpoch } = {}) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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 =
|
|
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
|
-
|
|
126
|
+
const ret = { vid }
|
|
127
|
+
if (hasCredits !== undefined) {
|
|
128
|
+
ret.hasCredits = hasCredits
|
|
129
|
+
}
|
|
130
|
+
return ret
|
|
126
131
|
}
|
|
127
132
|
}
|
|
128
133
|
|
package/src/video-object-name.js
DELETED
|
@@ -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
|
-
}
|