Skip to content

Commit

Permalink
feat: add custom data handler
Browse files Browse the repository at this point in the history
Resolves #241, by allowing developers to define their own `dataProvider` and
thus avoid making a network request when the platform already has data that can
be used by the player.

The `dataProvider` is a player `option` that takes a parameter function. This
function must take an argument symbolizing the content object to be played, in
the form of a `string`. Finally, this function must return a JSON object.

> [!TIP]
> How to use the `dataProvider` option

```javascript
const player = pillarbox('player', {
  srgOptions: {
    // defines the custom data provider
    dataProvider: (data) => JSON.parse(data)
  }
});

// A stringified object representing the mediaComposition
const mediaCompositionStringifiedObject = JSON.stringify({
  // ...
});

// load the src
player.src({src: mediaCompositionStringifiedObject, type:'srgssr/urn'});
```

> [!CAUTION]
> If the src object does not conform to the mediaComposition, unexpected errors
> may occur, preventing the video from playing.

- dataProvider's handleRequest no longer returns an instance of
mediaComposition, but a json object representing it
- srgssr middleware's getMediaComposition is responsible for instantiating the
mediaComposition
- add missing options to the srgOptions and create a dedicated type
- add dataProvider tutorial
- update unit tests
- change default URN for development page
  • Loading branch information
amtins committed Apr 3, 2024
1 parent 820b6e5 commit 7d924a4
Show file tree
Hide file tree
Showing 6 changed files with 105 additions and 54 deletions.
32 changes: 32 additions & 0 deletions docs/api/tutorials/Options.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,38 @@ See [Video.js Responsive Option](https://videojs.com/guides/options/#responsive)

The options in this section complement those of video.js, further customizing the player experience.

### `srgOptions.dataProvider`

***Specific to media content from SRG SSR.***

Specifies a custom function returning an object representing an SRG SSR media content. This function
takes a single parameter: the string initially passed as the player's source, and return a JSON
object representing the mediaComposition.

#### Usage

```javascript
const player = pillarbox('player', {
srgOptions: {
// Defines the custom data provider
dataProvider: (data) => JSON.parse(data)
}
});

// A stringified object representing the mediaComposition
const mediaCompositionStringifiedObject = JSON.stringify({
// ...
});

// Load the source with type 'srgssr/urn'
player.src({ src: mediaCompositionStringifiedObject, type: 'srgssr/urn' });
```

The returned object must conform to the [Media Composition](./MediaComposition.html) structure to
ensure proper media playback.

> The absence of a conforming object may lead to unexpected errors, affecting video playback.
### `srgOptions.dataProviderHost`

***Specific to media content from SRG SSR.***
Expand Down
13 changes: 12 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,15 @@
const ilHost = searchParams.get('ilHost') || undefined;
const language = searchParams.get('language');
const resize = searchParams.has('resize');
const urn = searchParams.get('urn') || 'urn:swi:video:48864812';
let urn = searchParams.get('urn');
const urlHandler = searchParams.has('urlHandler') ? url => url : undefined;

// Default content
if (!urn) {
urn = 'urn:rts:video:9883196';
currentTime = 911;
}

// Media examples
window.mediaExamples = {
_blockedSegmentAndChapters: {
Expand All @@ -61,6 +67,11 @@
src: 'urn:swi:video:48864812',
type: 'srgssr/urn'
},
_pyby: {
label: 'Pierre-Yves joueur de badminton at 911',
src: 'urn:rts:video:9883196',
type: 'srgssr/urn'
}
};

// Expose Pillarbox and player in the window object for debugging
Expand Down
7 changes: 3 additions & 4 deletions src/dataProvider/services/DataProvider.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import MediaComposition from '../model/MediaComposition.js';

/**
* Represents a data provider for constructing URLs and handling requests.
* @class
Expand Down Expand Up @@ -31,7 +29,7 @@ class DataProvider {
*
* @param {Function} urlHandler A function that constructs the URL
*
* @returns {Promise<MediaComposition>} A promise with the fetched data
* @returns {Promise<import('../model/MediaComposition.js').default>} A promise with the fetched data
*/
handleRequest(urlHandler) {
return async (urn) => {
Expand All @@ -42,9 +40,10 @@ class DataProvider {
throw response;
}

/** @type {import('../model/MediaComposition.js').default} */
const data = await response.json();

return Object.assign(new MediaComposition(), data);
return data;
};
}

Expand Down
13 changes: 6 additions & 7 deletions src/middleware/srgssr.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Image from '../utils/Image.js';
import Drm from '../utils/Drm.js';
import AkamaiTokenService from '../utils/AkamaiTokenService.js';
import SRGAnalytics from '../analytics/SRGAnalytics.js';
import MediaComposition from '../dataProvider/model/MediaComposition.js';

// Translations
import '../lang/de.js';
Expand Down Expand Up @@ -433,7 +434,9 @@ class SrgSsr {
urn,
handleRequest = new DataProvider().handleRequest()
) {
return handleRequest(urn);
const data = await handleRequest(urn);

return Object.assign(new MediaComposition(), data);
}

/**
Expand Down Expand Up @@ -643,14 +646,10 @@ Pillarbox.use('srgssr/urn', SrgSsr.middleware);

// Add Middleware specific options
Pillarbox.options.srgOptions = {
dataProvider: undefined,
dataProviderHost: undefined,
dataProviderUrlHandler: undefined,
tagCommanderScriptURL: undefined,
};

export default SrgSsr;

/**
* Ignored so that the link is resolved correctly in the API docs.
* @ignore
* @typedef {import('../dataProvider/model/MediaComposition.js').default} MediaComposition
*/
39 changes: 37 additions & 2 deletions src/middleware/typedef.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {MainResource} from '../dataProvider/model/typedef';
import {KeySystems} from '../utils/typedef';
import { MainResource } from '../dataProvider/model/typedef';
import { KeySystems } from '../utils/typedef';


/**
Expand Down Expand Up @@ -34,3 +34,38 @@ export type ComposedSrcMediaData = {
*/
mediaData: MainResource;
};

/**
* Represents a set of options specific to the SRG SSR.
*
* __Note__:
*
* - All these options have a default value and can therefore be undefined.
* - DataProvider options cannot be combined with each other.
*/
export type SrgOptions = {
/**
* A function returning an object representing a mediaComposition.
*
* @example
* // Must match the following signature
* (data: string) => any
*/
dataProvider: undefined | Function;
/**
* A specific host for a different IL environment.
*/
dataProviderHost: undefined | string;
/**
* A function for handling a custom data source.
*
* @example
* // Must match the following signature
* (contentId: string) => string
*/
dataProviderUrlHandler: undefined | Function;
/**
* The URL of the TagCommander script.
*/
tagCommanderScriptURL: undefined | string;
}
55 changes: 15 additions & 40 deletions test/dataProvider/services/DataProvider.spec.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import fetch from '../../__mocks__/fetch.js';
import '../../__mocks__/fetch.js';

import MediaComposition from '../../../src/dataProvider/model/MediaComposition.js';
import DataProvider from '../../../src/dataProvider/services/DataProvider.js';

describe('DataProvider', () => {
const urn10272382 = 'urn:rts:video:10272382';
const urn8414077 = 'urn:rts:video:8414077';
const urnNotFound = 'urn:not:found';
const dataproviderService = new DataProvider();

Expand All @@ -25,52 +23,29 @@ describe('DataProvider', () => {
*****************************************************************************
*/
describe('handleRequest', () => {
it('should return a mediaComposition object', async () => {
const spyObject = jest.spyOn(Object, 'assign');
const mediaComposition =
await dataproviderService.handleRequest()(urn10272382);
it('should use the default URL handler when the urlHandler is undefined', () => {
const spyOnMediaCompositionUrlHandler = jest.spyOn(dataproviderService, 'mediaCompositionUrlHandler');

expect(mediaComposition).toBeTruthy();
expect(spyObject).toHaveBeenCalled();
});

it('called multiple times should return a mediaComposition object', async () => {
const mediaCompositionUrn10272382 =
await dataproviderService.handleRequest()(urn10272382);
const defaultRequestHandler = dataproviderService.handleRequest();

const mediaCompositionUrn8414077 =
await dataproviderService.handleRequest()(urn8414077);
defaultRequestHandler(urn10272382);

expect(mediaCompositionUrn10272382).toBeTruthy();
expect(mediaCompositionUrn10272382.chapterUrn).toEqual(urn10272382);

expect(mediaCompositionUrn8414077).toBeTruthy();
expect(mediaCompositionUrn8414077.chapterUrn).toEqual(urn8414077);
expect(spyOnMediaCompositionUrlHandler).toHaveBeenCalledWith(urn10272382);
});

it('should be an instance of MediaComposition', async () => {
const mediaCompositionUrn10272382 =
await dataproviderService.handleRequest()(urn10272382);

expect(mediaCompositionUrn10272382).toBeInstanceOf(MediaComposition);
});
it('should not use the default URL handler if urlHandler is defined', () => {
const spyOnMediaCompositionUrlHandler = jest.spyOn(dataproviderService, 'mediaCompositionUrlHandler');
const defaultRequestHandler = dataproviderService.handleRequest((urn)=> urn);

it('should use a custom URL handler', async () => {
const customUrlHandler = (urn)=> urn;
const mediaCompositionUrn10272382 =
await dataproviderService.handleRequest(customUrlHandler)(urn10272382);
defaultRequestHandler(urn10272382);

expect(mediaCompositionUrn10272382).toBeInstanceOf(MediaComposition);
expect(spyOnMediaCompositionUrlHandler).not.toHaveBeenCalled();
});

it('should be rejected if URN does not exist', async () => {
await expect(
dataproviderService.handleRequest()(urnNotFound)
).rejects.not.toBeNull();
});
});
it('should throw an error if the urn does not exist', async () => {
const requestHandler = dataproviderService.handleRequest();

it('should satisfy eslint', async () => {
expect(fetch(urn10272382)).toBeTruthy();
await expect(requestHandler(urnNotFound)).rejects.not.toBeNull();
});
});
});

0 comments on commit 7d924a4

Please sign in to comment.