Skip to content

Commit

Permalink
feat(thumbnails): Add Player.getAllThumbnails (#5783)
Browse files Browse the repository at this point in the history
This method returns every thumbnail for a given image track ID, simplifying the process of determining how many thumbnails a given image track contains.

Closes #5781
  • Loading branch information
theodab committed Oct 19, 2023
1 parent 170a40c commit 9f7576b
Show file tree
Hide file tree
Showing 4 changed files with 143 additions and 15 deletions.
88 changes: 73 additions & 15 deletions lib/player.js
Expand Up @@ -3967,6 +3967,71 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
}
}

/**
* Returns Thumbnail objects for each thumbnail for a given image track ID.
*
* If the player has not loaded content, this will return a null.
*
* @param {number} trackId
* @return {!Promise.<?Array<!shaka.extern.Thumbnail>>}
* @export
*/
async getAllThumbnails(trackId) {
if (!this.manifest_) {
return null;
}
const imageStream = this.manifest_.imageStreams.find(
(stream) => stream.id == trackId);
if (!imageStream) {
return null;
}
if (!imageStream.segmentIndex) {
await imageStream.createSegmentIndex();
}
const promises = [];
imageStream.segmentIndex.forEachTopLevelReference((reference) => {
const dimensions = this.parseTilesLayout_(
reference.getTilesLayout() || imageStream.tilesLayout);
if (dimensions) {
const numThumbnails = dimensions.rows * dimensions.columns;
const duration = reference.trueEndTime - reference.startTime;
for (let i = 0; i < numThumbnails; i++) {
const sampleTime = reference.startTime + duration * i / numThumbnails;
promises.push(this.getThumbnails(trackId, sampleTime));
}
}
});
const thumbnails = await Promise.all(promises);
return thumbnails.filter((t) => t);
}

/**
* Parses a tiles layout.
*
* @param {string|undefined} tilesLayout
* @return {?{
* columns: number,
* rows: number
* }}
* @private
*/
parseTilesLayout_(tilesLayout) {
if (!tilesLayout) {
return null;
}
// This expression is used to detect one or more numbers (0-9) followed
// by an x and after one or more numbers (0-9)
const match = /(\d+)x(\d+)/.exec(tilesLayout);
if (!match) {
shaka.log.warning('Tiles layout does not contain a valid format ' +
' (columns x rows)');
return null;
}
const columns = parseInt(match[1], 10);
const rows = parseInt(match[2], 10);
return {columns, rows};
}

/**
* Return a Thumbnail object from a image track Id and time.
*
Expand All @@ -3992,23 +4057,16 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
return null;
}
const reference = imageStream.segmentIndex.get(referencePosition);
const tilesLayout =
reference.getTilesLayout() || imageStream.tilesLayout;
// This expression is used to detect one or more numbers (0-9) followed
// by an x and after one or more numbers (0-9)
const match = /(\d+)x(\d+)/.exec(tilesLayout);
if (!match) {
shaka.log.warning('Tiles layout does not contain a valid format ' +
' (columns x rows)');
const dimensions = this.parseTilesLayout_(
reference.getTilesLayout() || imageStream.tilesLayout);
if (!dimensions) {
return null;
}
const fullImageWidth = imageStream.width || 0;
const fullImageHeight = imageStream.height || 0;
const columns = parseInt(match[1], 10);
const rows = parseInt(match[2], 10);
let width = fullImageWidth / columns;
let height = fullImageHeight / rows;
const totalImages = columns * rows;
let width = fullImageWidth / dimensions.columns;
let height = fullImageHeight / dimensions.rows;
const totalImages = dimensions.columns * dimensions.rows;
const segmentDuration = reference.trueEndTime - reference.startTime;
const thumbnailDuration =
reference.getTileDuration() || (segmentDuration / totalImages);
Expand All @@ -4030,8 +4088,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
Math.floor((time - reference.startTime) / thumbnailDuration);
thumbnailTime = reference.startTime +
(thumbnailPosition * thumbnailDuration);
positionX = (thumbnailPosition % columns) * width;
positionY = Math.floor(thumbnailPosition / columns) * height;
positionX = (thumbnailPosition % dimensions.columns) * width;
positionY = Math.floor(thumbnailPosition / dimensions.columns) * height;
}
let sprite = false;
const thumbnailSprite = reference.getThumbnailSprite();
Expand Down
1 change: 1 addition & 0 deletions test/cast/cast_utils_unit.js
Expand Up @@ -21,6 +21,7 @@ describe('CastUtils', () => {
'getMediaElement', // Handled specially
'setMaxHardwareResolution',
'destroy', // Should use CastProxy.destroy instead
'getAllThumbnails', // Too large to proxy.
'drmInfo', // Too large to proxy
'getManifest', // Too large to proxy
'getManifestParserFactory', // Would not serialize.
Expand Down
6 changes: 6 additions & 0 deletions test/player_integration.js
Expand Up @@ -1332,6 +1332,9 @@ describe('Player', () => {
expect(thumbnail3.positionX).toBe(160);
expect(thumbnail3.positionY).toBe(90);
expect(thumbnail3.width).toBe(160);

const thumbnails = await player.getAllThumbnails(newTrack.id);
expect(thumbnails.length).toBe(3);
});

it('appends thumbnails for external thumbnails without sprites',
Expand All @@ -1355,6 +1358,9 @@ describe('Player', () => {
const thumbnail3 = await player.getThumbnails(newTrack.id, 40);
expect(thumbnail3.startTime).toBe(30);
expect(thumbnail3.duration).toBe(30);

const thumbnails = await player.getAllThumbnails(newTrack.id);
expect(thumbnails.length).toBe(3);
});
}); // describe('addThumbnailsTrack')
});
63 changes: 63 additions & 0 deletions test/player_unit.js
Expand Up @@ -4179,6 +4179,69 @@ describe('Player', () => {
expect(thumbnail8.duration).toBe(10);
});
});

describe('getAllThumbnails', () => {
it('returns all thumbnails', async () => {
const uris = () => ['thumbnail'];
const ref = new shaka.media.SegmentReference(
0, 60, uris, 0, null, null, 0, 0, Infinity, [],
);
const index = new shaka.media.SegmentIndex([ref]);

manifest = shaka.test.ManifestGenerator.generate((manifest) => {
manifest.addVariant(0, (variant) => {
variant.addVideo(1);
});
manifest.addImageStream(5, (stream) => {
stream.originalId = 'thumbnail';
stream.width = 200;
stream.height = 150;
stream.mimeType = 'image/jpeg';
stream.tilesLayout = '2x3';
stream.segmentIndex = index;
});
});

await player.load(fakeManifestUri, 0, fakeMimeType);

expect(player.getImageTracks()[0].width).toBe(100);
expect(player.getImageTracks()[0].height).toBe(50);
const thumbnails = await player.getAllThumbnails(5);
expect(thumbnails.length).toBe(6);
expect(thumbnails[0]).toEqual(jasmine.objectContaining({
imageHeight: 150,
imageWidth: 200,
positionX: 0,
positionY: 0,
width: 100,
height: 50,
}));
expect(thumbnails[1]).toEqual(jasmine.objectContaining({
imageHeight: 150,
imageWidth: 200,
positionX: 100,
positionY: 0,
width: 100,
height: 50,
}));
expect(thumbnails[2]).toEqual(jasmine.objectContaining({
imageHeight: 150,
imageWidth: 200,
positionX: 0,
positionY: 50,
width: 100,
height: 50,
}));
expect(thumbnails[5]).toEqual(jasmine.objectContaining({
imageHeight: 150,
imageWidth: 200,
positionX: 100,
positionY: 100,
width: 100,
height: 50,
}));
});
});
});

describe('config streaming.startAtSegmentBoundary', () => {
Expand Down

0 comments on commit 9f7576b

Please sign in to comment.