Skip to content

Commit

Permalink
feat: play unprotected SRG SSR contents from urns
Browse files Browse the repository at this point in the history
Allows playback of any unprotected SRG SSR content from its URN.

- adds `DataProvider` which is in charge of retrieving the `MediaComposition`
- adds `SrgSsr` middleware for resolving `srgssr/urn` sources
- updates demo with sample sources
- adds test coverage for `DataProvider` and `SrgSsr` classes
- adds mock files

Resolves #24
  • Loading branch information
amtins committed Aug 11, 2023
1 parent bdece55 commit 64e32b9
Show file tree
Hide file tree
Showing 11 changed files with 3,649 additions and 156 deletions.
18 changes: 14 additions & 4 deletions demo/src/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Pillarbox from '../../src/pillarbox.js';
import '../../src/middleware/srgssr.js';

// Get pillarbox version
document.querySelector('.version').textContent = Pillarbox.VERSION.pillarbox;
Expand All @@ -15,8 +16,17 @@ window.pillarbox = Pillarbox;
// Expose the player in the window object
window.player = player;

// Source examples
window.sourceExamples = {
bipbop: {
src: 'https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8',
type: 'application/x-mpegURL',
},
urn: {
src: 'urn:rts:video:14160770',
type: 'srgssr/urn',
},
};

// Load the source
player.src({
src: 'https://d2zihajmogu5jn.cloudfront.net/bipbop-advanced/bipbop_16x9_variant.m3u8',
type: 'application/x-mpegURL',
});
player.src(window.sourceExamples.bipbop);
44 changes: 0 additions & 44 deletions src/dataProvider/model/MediaComposition.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,6 @@ class MediaComposition {
return undefined;
}

/**
* Find all DRM vendors available for a resource.
*
* @param {Resource} resource
*
* @returns {Array} of DRM vendors
*/
static findDrmListByResource(resource) {
return resource.drmList;
}

/**
* Return a segment from main chapter following segmentUrn in mediaComposition.
*
Expand Down Expand Up @@ -284,39 +273,6 @@ class MediaComposition {

return resourceList || [];
}

/**
* Get subdivisions to be displayed for this MediaComposition.
* They can be segments or chapters.
*
* @returns {Array} of subdivisions
*/
getSubdivisions() {
const chapters = this.getChapters();
const mainChapter = this.getMainChapter();
const subdivisions = [];
const displayableMainSegments = this.getMainSegments().filter(
(s) => s.displayable
);
const hasDisplayableSegments = Boolean(displayableMainSegments.length);

if (chapters.length === 1 && !hasDisplayableSegments) {
return [];
}

chapters.forEach((chapter) => {
const isSegment =
chapter.urn === mainChapter.urn && hasDisplayableSegments;

if (isSegment) {
subdivisions.push(...displayableMainSegments);
} else {
subdivisions.push(chapter);
}
});

return subdivisions;
}
}

export default MediaComposition;
49 changes: 49 additions & 0 deletions src/dataProvider/services/DataProviderService.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import MediaComposition from '../model/MediaComposition.js';

/**
* @ignore
*/
class DataProviderService {
constructor(hostName = 'il.srgssr.ch') {
this.setIlHost(hostName);
}

setIlHost(hostName) {
this.baseUrl = `${hostName}/integrationlayer/2.0/`;
}

/**
* Get media composition by URN.
*
* @param {String} urn urn:rts:video:9800629
* @param {Boolean} onlyChapters
* @returns {Object} media composition json object
*/
getMediaCompositionByUrn(urn, onlyChapters = false) {
const url = `https://${this.baseUrl}mediaComposition/byUrn/${urn}?onlyChapters=${onlyChapters}&vector=portalplay`;

return fetch(url)
.then((response) => {
if (response.ok) {
return response.json().then((data) => {
const mediaComposition = Object.assign(
new MediaComposition(),
data,
{ onlyChapters }
);

return {
mediaComposition,
};
});
}

return Promise.reject(response);
})
.catch((reason) => {
return Promise.reject(reason);
});
}
}

export default DataProviderService;
67 changes: 67 additions & 0 deletions src/middleware/srgssr.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import Pillarbox from '../../src/pillarbox.js';
import DataProviderService from '../dataProvider/services/DataProviderService.js';
import Image from '../utils/Image.js';

class SrgSsr {
static async getMediaComposition(
urn,
dataProvider = new DataProviderService()
) {
return dataProvider.getMediaCompositionByUrn(urn);
}

static getSource({ url, mimeType }) {
return {
src: url,
type: mimeType,
};
}

static updatePoster(player, mediaComposition, imageService = Image) {
player.poster(
imageService.scale({
url: mediaComposition.getMainChapterImageUrl(),
})
);
}

static updateTitleBar(player, mediaComposition) {
player.titleBar.update({
title: mediaComposition.getMainChapter().vendor,
description: mediaComposition.getMainChapter().title,
});
}

static middleware(
player,
dataProvider = new DataProviderService(
player.options()?.srgOptions?.dataProviderHost
),
imageService = Image
) {
return {
setSource: async (srcObj, next) => {
try {
const { mediaComposition } = await SrgSsr.getMediaComposition(
srcObj.src,
dataProvider
);
const [mediaInfo] = mediaComposition.getMainResources();
const mediaSrc = SrgSsr.getSource(mediaInfo);
const srcMediaObj = Pillarbox.obj.merge({}, mediaInfo, mediaSrc);

SrgSsr.updateTitleBar(player, mediaComposition);
SrgSsr.updatePoster(player, mediaComposition, imageService);

return next(null, srcMediaObj);
} catch (error) {
return next(error);
}
},
};
}
}

Pillarbox.use('srgssr/urn', SrgSsr.middleware);

export default SrgSsr;
37 changes: 37 additions & 0 deletions test/__mocks__/fetch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import * as urn6735513 from './urn:rts:video:6735513.json';
import * as urn8414077 from './urn:rts:video:8414077.json';
import * as urn10272382 from './urn:rts:video:10272382.json';
import * as urn10313496 from './urn:rts:video:10313496.json';
import * as urn10342901 from './urn:rts:video:10342901.json';
import * as urn10348175 from './urn:rts:video:10348175.json';
import * as urn10373456 from './urn:rts:video:10373456.json';

const urns = {
'urn:rts:video:6735513': urn6735513,
'urn:rts:video:8414077': urn8414077,
'urn:rts:video:10272382': urn10272382,
'urn:rts:video:10313496': urn10313496,
'urn:rts:video:10342901': urn10342901,
'urn:rts:video:10348175': urn10348175,
'urn:rts:video:10373456': urn10373456,
};

const fetch = jest.fn((url, onlyChapters) => {
const urn = url
.split('/')
.pop()
.split('?')[0];

return Promise.resolve({
ok: urns[urn] !== undefined,
json() {
if (urns[urn] === undefined || onlyChapters === '') {
return Promise.reject(new Error(`${urn} mocked URN does not exist`));
}

return Promise.resolve(urns[urn]);
},
});
});

export default (global.fetch = fetch);
Loading

0 comments on commit 64e32b9

Please sign in to comment.