Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add getThumbnail APIs to Player and Source #225

Merged
merged 4 commits into from
Sep 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Player.getThumbnail has been deprecated in favour of Source.getThumbnail on Android. From my perspective I would be in favour of only wrapping Source.getThumbnail in this PR, but I'm not sure what the state is on iOS, just wanted to point this out.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zigavehovec I know about the deprecation on Android but I went for providing both as our Source API is not that rich in the React Native SDK yet. Also we don't support playlist, so only one Source is available at a 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;
}