Pixellot offers automated sports production solutions that provide affordable alternatives to traditional video capture, production, and distribution systems for professional and semi-professional sports events. Founded in 2013, Pixellot’s AI technology solution streamlines production workflow by deploying an unmanned multicamera system in a fixed location, with additional angles as required, to cover the entire field, offering a stitched panoramic image. Advanced algorithms enable automatic coverage of the flow of play and generate highlights. Pixellot systems are deployed by broadcasters, production companies, clubs, federations, universities, high schools, sports portals, and coaching solution providers around the globe.
For more information visit: www.pixellot.tv
This document contains proprietary and confidential material of Pixellot Ltd. Any unauthorized reproduction, use, or disclosure of this material, or any part thereof, is strictly prohibited. This document is solely for the use of Pixellot employees and authorized Pixellot customers.
The material furnished in this document is believed to be accurate and reliable. However, no responsibility is assumed by Pixellot Ltd. for the use of this document or any material included herein. Pixellot Ltd. reserves the right to make changes to this document or any material included herein at any time and without notice.
For more information visit: www.pixellot.tv
Copyright © 2024 Pixellot Ltd.
All Rights Reserved.
Pixellot provides its partners with a dedicated Partner API for creating new entities, updating and retrieving them. In addition, a partner can subscribe to Pixellot webhooks to get notification about different significant occurrences.
The idea is that a partner can, for
instance, ask for a list of events on startup. When the partner
subscribes to the webhooks service, it will get notifications about
every significant change (including creation and deletion) and will
update its own database. This will omit the need to repeatedly ask for
the full list of events to get new events and updates to existing
events. The updates can, in this way, be done passively in a push method
instead of pull.
Explore Partner API through our official documentation. This resource provides essential information for integrating our services seamlessly into your applications. Find detailed API endpoints, request methods, authentication guides, and error handling instructions.
For more information, visit our REST API documentation at https://app.swaggerhub.com/apis/Pixellot/partner_api/
Available environments:
[Base URL] = api.pixellot.tv/v1 (PROD) - production workloads
[Base URL] = api.stage.pixellot.tv/v1 (STAGE) - tests and integration
All
API requests, except for login, require the “Authorization” header to
be present. Authorization is based on the JWT method. JSON Web Token
(JWT) is a compact, URL-safe means of representing claims to be
transferred between two parties.
Typical authenticated request would be having the following look:
curl https://api.stage.pixellot.tv/v1/venues -H "Authorization: eyJ0eX..."
This doc includes materials that help other companies integrate with the Pixellot Advanced Event Breakdown feature:
Pixellot subscription management endpoints
Advanced Event Breakdown schema definitions
Pixellot provides a convenient Pixellot to 3rd party company communication approach. It is a subscription mechanism, where 3rd party company gives a URL to Pixellot that needs to be called when an action happens. How does it work in a nutshell - 3rd party company subscribes to a specific topic, like EventBreakdown, by providing an HTTP webhook URL that will be called once an action happens.
The subscriptions are for notification purposes only - the HTTP response is ignored, except for failures. If a subscription has many failures - an email about it will be sent to the email that was specified in the subscription creation body.
The Login endpoint should be called to get the API token. A request createSubscription should be called with the messageType field set to EventBreakdown.
Subscription request example:
curl --request POST 'https://api.pixellot.tv/v1/monitoring/subscriptions' \
--header 'Authorization: TOKEN' \
--header 'Content-Type: application/json' \
--data-raw '{
"messageType": "EventBreakdown",
"tenant": "TENANT",
"url": "https://example.compaby.domain.com/event-breakdown",
"emails": "company@company.com",
"secret": "somesecretvalue"
}'
After subscribing to the event changes, requests will be sent to the URL specified in the request body. Messages will be in the format of EventBreakdownHookMessageBody
The app is created with NodeJS. It uses the ffmpef-fluent to build ffmpeg command programmatically
and moment to do the date and time operations.
The app generates 3 clips for a soccer advanced event breakdown: all shots, home team shots, and away team shots. generateClips function needs 2 arguments - the event and the breakdown. The event is sent in the EventBreakdownHookMessageBody in the event property.
package.json
{
"name": "advanced-event-breakdown",
"main": "index.js",
"dependencies": {
"fluent-ffmpeg": "^2.1.2",
"moment": "^2.29.4"
}
}
index.js
const fs = require('fs');
const path = require('path');
const moment = require('moment');
const FfmpegCommand = require('fluent-ffmpeg');
const TMP_DIR_PATH = path.resolve(__dirname, 'tmp');
/**
* Function takes the event breakdown and returns only Shot tags
*/
const getAllShotTags = (eventBreakdown) => eventBreakdown.filter((tag) => tag.tagResource.name === 'Shot');
/**
* Function takes the event breakdown and returns only Shot tags of a specific team
*/
const getTeamShotTags = (eventBreakdown, teamName) => eventBreakdown.filter((tag) => {
const isShotTag = tag.tagResource.name === 'Shot';
if (isShotTag) {
const teamTagAttribure = tag.tagAttributes.find((tagAttribute) => tagAttribute.name === 'Team');
if (teamTagAttribure && teamTagAttribure.value === teamName) {
return true;
}
}
return false;
});
/**
* Function creates one clip from the provided tags
* Step 1: Clean the tmp directory
* Step 2: Covert the tag startOffset and endOffset to the format that is respected by the ffmpeg
* { clipStart: number, clipDuration: number },
* where `clipStart` is start of the clip on the source video in seconds and clipDuration is the duration of the clip
* Step 3: Initialize `chunksFileContent` variable to an empty string. This variable will hold the content that will be written to the chunks.txt
* chunks.txt will be provided to ffmpeg to concat generated clips
* Step 4: Create clips and save them to the tmp folder
* Step 5: Write the `chunksFileContent` to the chunks.txt file
* Step 5: Concat the files and save the clip to the `outputFilePath`
*/
const createClipFromTags = async (videoUrl, tags, outputFilePath) => {
// Step 1
fs.readdirSync(TMP_DIR_PATH).forEach((f) => fs.rmSync(`${TMP_DIR_PATH}/${f}`));
// Step 2
const ffmpegTimestamps = tags.map(({ startOffset, endOffset }) => {
const startOffsetSeconds = moment.duration(startOffset).asSeconds();
const endOffsetDurationSeconds = moment.duration(endOffset).asSeconds();
return {
clipStart: startOffsetSeconds,
clipDuration: endOffsetDurationSeconds - startOffsetSeconds,
};
});
// Step 3
let chunksFileContent = '';
// Step 4
await Promise.all(ffmpegTimestamps.map(({ clipDuration, clipStart }) => {
const fileName = `${clipStart}-${clipDuration}.mp4`;
chunksFileContent += `file ${fileName}\n`;
return new Promise((resolve) => {
new FfmpegCommand()
.addInput(videoUrl)
.addOption('-ss', clipStart)
.addOption('-t', clipDuration)
.addOutput(path.resolve(TMP_DIR_PATH, `${clipStart}-${clipDuration}.mp4`))
.on('end', resolve)
.run();
});
}));
// Step 5
fs.writeFileSync(path.resolve(TMP_DIR_PATH, 'chunks.txt'), chunksFileContent);
// Step 6
await new Promise((resolve) => {
new FfmpegCommand()
.input(path.resolve(TMP_DIR_PATH, 'chunks.txt'))
.inputOptions(['-f concat', '-safe 0'])
.outputOptions('-c copy')
.on('end', resolve)
.save(outputFilePath);
});
};
const generateClips = async (event, eventBreakdown) => {
const {
labels: {
teams: {
homeTeamName,
awayTeamName,
},
},
locallyRecordedHdMp4Url,
} = event;
const shotsTags = getAllShotTags(eventBreakdown);
const homeTeamShots = getTeamShotTags(eventBreakdown, homeTeamName);
const awayTeamShots = getTeamShotTags(eventBreakdown, awayTeamName);
await createClipFromTags(locallyRecordedHdMp4Url, shotsTags, path.resolve(__dirname, 'clips', 'allShots.mp4'));
await createClipFromTags(locallyRecordedHdMp4Url, homeTeamShots, path.resolve(__dirname, 'clips', 'homeTeamShots.mp4'));
await createClipFromTags(locallyRecordedHdMp4Url, awayTeamShots, path.resolve(__dirname, 'clips', 'awayTeamShots.mp4'));
};
generateClips(
{
labels: {
teams: {
homeTeamName: 'Home',
awayTeamName: 'Away',
},
},
locallyRecordedHdMp4Url: path.resolve(__dirname, 'video', 'video.mp4'),
},
[
{
startOffset: '00:02:00',
endOffset: '00:02:05',
tagResource: {
name: 'Shot',
},
tagAttributes: [
{
name: 'Team',
value: 'Home',
},
],
},
{
startOffset: '00:03:00',
endOffset: '00:03:05',
tagResource: {
name: 'Shot',
},
tagAttributes: [
{
name: 'Team',
value: 'Away',
},
],
},
{
startOffset: '00:04:00',
endOffset: '00:04:05',
tagResource: {
name: 'Shot',
},
tagAttributes: [
{
name: 'Team',
value: 'Away',
},
],
},
],
);