Skip to content

Commit

Permalink
Merge pull request #225 from bitmovin/add-thumbnail-API
Browse files Browse the repository at this point in the history
Add `getThumbnail` APIs to `Player` and `Source`
  • Loading branch information
rolandkakonyi authored Sep 7, 2023
2 parents a5d2cee + 98401ce commit 28f0d75
Show file tree
Hide file tree
Showing 13 changed files with 176 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

- `Player.getAudioTrack` and `Player.getSubtitleTrack` APIs to get currently selected audio and subtitle tracks
- `SourceConfig.description` property to allow setting a description for the source
- `Player.getThumbnail` and `Source.getThumbnail` APIs to get thumbnail images

## [0.10.0] (2023-09-04)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,18 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB
}
}

/**
* Returns the thumbnail image for the active `Source` at a certain time.
* @param nativeId Target player id.
* @param time Playback time for the thumbnail.
*/
@ReactMethod
fun getThumbnail(nativeId: NativeId, time: Double, promise: Promise) {
uiManager()?.addUIBlock {
promise.resolve(JsonConverter.fromThumbnail(players[nativeId]?.source?.getThumbnail(time)))
}
}

/**
* Helper function that returns the initialized `UIManager` instance.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,6 @@ class SourceModule(private val context: ReactApplicationContext) : ReactContextB
/**
* Set the metadata for a loaded `nativeId` source.
* @param nativeId Source `nativeId`.
* @param promise: JS promise object.
*/
@ReactMethod
fun setMetadata(nativeId: NativeId, metadata: ReadableMap?) {
Expand All @@ -149,6 +148,18 @@ class SourceModule(private val context: ReactApplicationContext) : ReactContextB
}
}

/**
* Returns the thumbnail image for the `Source` at a certain time.
* @param nativeId Target player id.
* @param time Playback time for the thumbnail.
*/
@ReactMethod
fun getThumbnail(nativeId: NativeId, time: Double, promise: Promise) {
uiManager()?.addUIBlock {
promise.resolve(JsonConverter.fromThumbnail(sources[nativeId]?.getThumbnail(time)))
}
}

/**
* Helper method that converts a React `ReadableMap` into a kotlin String -> String map.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import com.bitmovin.player.api.event.data.SeekPosition
import com.bitmovin.player.api.media.AdaptationConfig
import com.bitmovin.player.api.media.audio.AudioTrack
import com.bitmovin.player.api.media.subtitle.SubtitleTrack
import com.bitmovin.player.api.media.thumbnail.Thumbnail
import com.bitmovin.player.api.media.thumbnail.ThumbnailTrack
import com.bitmovin.player.api.media.video.quality.VideoQuality
import com.bitmovin.player.api.offline.options.OfflineContentOptions
Expand Down Expand Up @@ -937,5 +938,23 @@ class JsonConverter {
putArray("textOptions", options.textOptions.map { toJson(it) }.toReadableArray())
}
}

@JvmStatic
fun fromThumbnail(thumbnail: Thumbnail?): WritableMap? {
if (thumbnail == null) {
return null
}

return Arguments.createMap().apply {
putDouble("start", thumbnail.start)
putDouble("end", thumbnail.end)
putString("text", thumbnail.text)
putString("url", thumbnail.uri.toString())
putInt("x", thumbnail.x)
putInt("y", thumbnail.y)
putInt("width", thumbnail.width)
putInt("height", thumbnail.height)
}
}
}
}
5 changes: 5 additions & 0 deletions ios/PlayerModule.m
Original file line number Diff line number Diff line change
Expand Up @@ -92,5 +92,10 @@ @interface RCT_EXTERN_REMAP_MODULE(PlayerModule, PlayerModule, NSObject)
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(setMaxSelectableBitrate:(NSString *)nativeId maxSelectableBitrate:(nonnull NSNumber *)maxSelectableBitrate)
RCT_EXTERN_METHOD(
getThumbnail:(NSString *)nativeId
time:(nonnull NSNumber *)time
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)

@end
18 changes: 18 additions & 0 deletions ios/PlayerModule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -564,4 +564,22 @@ class PlayerModule: NSObject, RCTBridgeModule {
self?.players[nativeId]?.maxSelectableBitrate = maxSelectableBitrateValue != -1 ? maxSelectableBitrateValue : 0
}
}

/**
Returns the thumbnail image for the active `Source` at a certain time.
- Parameter nativeId: Target player id.
- Parameter resolver: JS promise resolver.
- Parameter rejecter: JS promise rejecter.
*/
@objc(getThumbnail:time:resolver:rejecter:)
func getThumbnail(
_ nativeId: NativeId,
time: NSNumber,
resolver resolve: @escaping RCTPromiseResolveBlock,
rejecter reject: @escaping RCTPromiseRejectBlock
) {
bridge.uiManager.addUIBlock { [weak self] _, _ in
resolve(RCTConvert.toJson(thumbnail: self?.players[nativeId]?.thumbnail(forTime: time.doubleValue)))
}
}
}
22 changes: 22 additions & 0 deletions ios/RCTConvert+BitmovinPlayer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -935,4 +935,26 @@ extension RCTConvert {

return adaptationConfig
}

/**
Utility method to compute a JS value from an `Thumbnail` object.
- Parameter thumbnail `Thumbnail` object to be converted.
- Returns: The produced JS object.
*/
static func toJson(thumbnail: Thumbnail?) -> [String: Any?]? {
guard let thumbnail = thumbnail else {
return nil
}

return [
"start": thumbnail.start,
"end": thumbnail.end,
"text": thumbnail.text,
"url": thumbnail.url.absoluteString,
"x": thumbnail.x,
"y": thumbnail.y,
"width": thumbnail.width,
"height": thumbnail.height,
]
}
}
5 changes: 5 additions & 0 deletions ios/SourceModule.m
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,10 @@ @interface RCT_EXTERN_REMAP_MODULE(SourceModule, SourceModule, NSObject)
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(setMetadata:(NSString *)nativeId metadata:(nullable id)metadata)
RCT_EXTERN_METHOD(
getThumbnail:(NSString *)nativeId
time:(nonnull NSNumber *)time
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)

@end
18 changes: 18 additions & 0 deletions ios/SourceModule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -184,4 +184,22 @@ class SourceModule: NSObject, RCTBridgeModule {
self?.sources[nativeId]?.metadata = metadata
}
}

/**
Returns the thumbnail image for the `Source` at a certain time.
- Parameter nativeId: Target player id.
- Parameter resolver: JS promise resolver.
- Parameter rejecter: JS promise rejecter.
*/
@objc(getThumbnail:time:resolver:rejecter:)
func getThumbnail(
_ nativeId: NativeId,
time: NSNumber,
resolver resolve: @escaping RCTPromiseResolveBlock,
rejecter reject: @escaping RCTPromiseRejectBlock
) {
bridge.uiManager.addUIBlock { [weak self] _, _ in
resolve(RCTConvert.toJson(thumbnail: self?.sources[nativeId]?.thumbnail(forTime: time.doubleValue)))
}
}
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ export * from './subtitleTrack';
export * from './styleConfig';
export * from './ui';
export * from './offline';
export * from './thumbnail';
13 changes: 13 additions & 0 deletions src/player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { StyleConfig } from './styleConfig';
import { TweaksConfig } from './tweaksConfig';
import { AdaptationConfig } from './adaptationConfig';
import { OfflineContentManager, OfflineSourceOptions } from './offline';
import { Thumbnail } from './thumbnail';

const PlayerModule = NativeModules.PlayerModule;

Expand Down Expand Up @@ -472,4 +473,16 @@ export class Player extends NativeInstance<PlayerConfig> {
setMaxSelectableBitrate = (bitrate: number | null) => {
PlayerModule.setMaxSelectableBitrate(this.nativeId, bitrate || -1);
};

/**
* @returns a `Thumbnail` for the specified playback time for the currently active source if available.
* Supported thumbnail formats are:
* - `WebVtt` configured via `SourceConfig.thumbnailTrack`, on all supported platforms
* - HLS `Image Media Playlist` in the multivariant playlist, Android-only
* - DASH `Image Adaptation Set` as specified in DASH-IF IOP, Android-only
* If a `WebVtt` thumbnail track is provided, any potential in-manifest thumbnails are ignored on Android.
*/
getThumbnail = async (time: number): Promise<Thumbnail | null> => {
return PlayerModule.getThumbnail(this.nativeId, time);
};
}
13 changes: 13 additions & 0 deletions src/source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { NativeModules } from 'react-native';
import { Drm, DrmConfig } from './drm';
import NativeInstance, { NativeInstanceConfig } from './nativeInstance';
import { SideLoadedSubtitleTrack } from './subtitleTrack';
import { Thumbnail } from './thumbnail';

const SourceModule = NativeModules.SourceModule;

Expand Down Expand Up @@ -222,4 +223,16 @@ export class Source extends NativeInstance<SourceConfig> {
loadingState = async (): Promise<LoadingState> => {
return SourceModule.loadingState(this.nativeId);
};

/**
* @returns a `Thumbnail` for the specified playback time if available.
* Supported thumbnail formats are:
* - `WebVtt` configured via `SourceConfig.thumbnailTrack`, on all supported platforms
* - HLS `Image Media Playlist` in the multivariant playlist, Android-only
* - DASH `Image Adaptation Set` as specified in DASH-IF IOP, Android-only
* If a `WebVtt` thumbnail track is provided, any potential in-manifest thumbnails are ignored on Android.
*/
getThumbnail = async (time: number): Promise<Thumbnail | null> => {
return SourceModule.getThumbnail(this.nativeId, time);
};
}
37 changes: 37 additions & 0 deletions src/thumbnail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* Represents a VTT thumbnail.
*/
export interface Thumbnail {
/**
* The start time of the thumbnail.
*/
start: number;
/**
* The end time of the thumbnail.
*/
end: number;
/**
* The raw cue data.
*/
text: string;
/**
* The URL of the spritesheet
*/
url: string;
/**
* The horizontal offset of the thumbnail in its spritesheet
*/
x: number;
/**
* The vertical offset of the thumbnail in its spritesheet
*/
y: number;
/**
* The width of the thumbnail
*/
width: number;
/**
* The height of the thumbnail
*/
height: number;
}

0 comments on commit 28f0d75

Please sign in to comment.