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

Expose timeshift api #135

Merged
merged 16 commits into from
Jun 28, 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
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,18 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB
}
}

/**
* Call `.timeShift(offset:)` on `nativeId`'s player.
* @param nativeId Target player Id.
* @param offset Offset time in seconds.
*/
@ReactMethod
fun timeShift(nativeId: NativeId, offset: Double) {
uiManager()?.addUIBlock {
players[nativeId]?.timeShift(offset)
}
}

/**
* Call `.mute()` on `nativeId`'s player.
* @param nativeId Target player Id.
Expand Down Expand Up @@ -362,6 +374,30 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB
}
}

/**
* The current time shift of the live stream in seconds. This value is always 0 if the active [source] is not a
* live stream or there is no active playback session.
* @param nativeId Target player id.
*/
@ReactMethod
fun getTimeShift(nativeId: NativeId, promise: Promise) {
uiManager()?.addUIBlock {
promise.resolve(players[nativeId]?.timeShift)
}
}

/**
* The limit in seconds for time shifting. This value is either negative or 0 and it is always 0 if the active
* [source] is not a live stream or there is no active playback session.
* @param nativeId Target player id.
*/
@ReactMethod
fun getMaxTimeShift(nativeId: NativeId, promise: Promise) {
uiManager()?.addUIBlock {
promise.resolve(players[nativeId]?.maxTimeShift)
}
}

/**
* Helper function that returns the initialized `UIManager` instance.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ private val EVENT_CLASS_TO_REACT_NATIVE_NAME_MAPPING = mapOf(
PlayerEvent.PlaybackFinished::class to "playbackFinished",
PlayerEvent.Seek::class to "seek",
PlayerEvent.Seeked::class to "seeked",
PlayerEvent.TimeShift::class to "timeShift",
PlayerEvent.TimeShifted::class to "timeShifted",
PlayerEvent.StallStarted::class to "stallStarted",
PlayerEvent.StallEnded::class to "stallEnded",
PlayerEvent.TimeChanged::class to "timeChanged",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ class RNPlayerViewManager(private val context: ReactApplicationContext) : Simple
"playbackFinished" to "onPlaybackFinished",
"seek" to "onSeek",
"seeked" to "onSeeked",
"timeShift" to "onTimeShift",
"timeShifted" to "onTimeShifted",
"stallStarted" to "onStallStarted",
"stallEnded" to "onStallEnded",
"timeChanged" to "onTimeChanged",
Expand Down Expand Up @@ -149,7 +151,7 @@ class RNPlayerViewManager(private val context: ReactApplicationContext) : Simple
Commands.ATTACH_FULLSCREEN_BRIDGE.ordinal -> args?.getString(1)?.let { fullscreenBridgeId ->
attachFullscreenBridge(view, fullscreenBridgeId)
}
Commands.SET_CUSTOM_MESSAGE_HANDLER_BRIDGE_ID.ordinal -> {
Commands.SET_CUSTOM_MESSAGE_HANDLER_BRIDGE_ID.ordinal -> {
args?.getString(1)?.let { customMessageHandlerBridgeId ->
setCustomMessageHandlerBridgeId(view, customMessageHandlerBridgeId)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,10 @@ class JsonConverter {
json.putMap("from", fromSeekPosition(event.from))
json.putMap("to", fromSeekPosition(event.to))
}
if (event is PlayerEvent.TimeShift) {
json.putDouble("position", event.position)
json.putDouble("targetPosition", event.target)
}
if (event is PlayerEvent.PictureInPictureAvailabilityChanged) {
json.putBoolean("isPictureInPictureAvailable", event.isPictureInPictureAvailable)
}
Expand Down
11 changes: 11 additions & 0 deletions ios/Event+JSON.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,17 @@ extension SeekEvent {
}
}

extension TimeShiftEvent {
func toJSON() -> [AnyHashable: Any] {
[
"name": name,
"timestamp": timestamp,
"position": position,
"targetPosition": target
]
}
}

extension TimeChangedEvent {
func toJSON() -> [AnyHashable: Any] {
["name": name, "timestamp": timestamp, "currentTime": currentTime]
Expand Down
9 changes: 9 additions & 0 deletions ios/PlayerModule.m
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ @interface RCT_EXTERN_REMAP_MODULE(PlayerModule, PlayerModule, NSObject)
RCT_EXTERN_METHOD(play:(NSString *)nativeId)
RCT_EXTERN_METHOD(pause:(NSString *)nativeId)
RCT_EXTERN_METHOD(seek:(NSString *)nativeId time:(nonnull NSNumber *)time)
RCT_EXTERN_METHOD(timeShift:(NSString *)nativeId offset:(nonnull NSNumber *)time)
RCT_EXTERN_METHOD(mute:(NSString *)nativeId)
RCT_EXTERN_METHOD(unmute:(NSString *)nativeId)
RCT_EXTERN_METHOD(destroy:(NSString *)nativeId)
Expand Down Expand Up @@ -73,5 +74,13 @@ @interface RCT_EXTERN_REMAP_MODULE(PlayerModule, PlayerModule, NSObject)
isAd:(NSString *)nativeId
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(
getTimeShift:(NSString *)nativeId
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(
getMaxTimeShift:(NSString *)nativeId
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)

@end
47 changes: 47 additions & 0 deletions ios/PlayerModule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,18 @@ class PlayerModule: NSObject, RCTBridgeModule {
self?.players[nativeId]?.seek(time: time.doubleValue)
}
}

/**
Sets `timeShift` on `nativeId`'s player.
- Parameter nativeId: Target player Id.
- Parameter offset: Offset to timeShift to in seconds.
*/
@objc(timeShift:offset:)
func timeShift(_ nativeId: NativeId, offset: NSNumber) {
bridge.uiManager.addUIBlock { [weak self] _, _ in
self?.players[nativeId]?.timeShift = offset.doubleValue
}
}

/**
Call `.mute()` on `nativeId`'s player.
Expand Down Expand Up @@ -450,4 +462,39 @@ class PlayerModule: NSObject, RCTBridgeModule {
resolve(self?.players[nativeId]?.isAd)
}
}

/**
The current time shift of the live stream in seconds. This value is always 0 if the active `source` is not a
live stream or there are no sources loaded.
- Parameter nativeId: Target player id.
- Parameter resolver: JS promise resolver.
- Parameter rejecter: JS promise rejecter.
*/
@objc(getTimeShift:resolver:rejecter:)
func getTimeShift(
_ nativeId: NativeId,
resolver resolve: @escaping RCTPromiseResolveBlock,
rejecter reject: @escaping RCTPromiseRejectBlock
) {
bridge.uiManager.addUIBlock { [weak self] _, _ in
resolve(self?.players[nativeId]?.timeShift)
}
}

/**
Returns the limit in seconds for time shift. Is either negative or 0. Is applicable for live streams only.
- Parameter nativeId: Target player id.
- Parameter resolver: JS promise resolver.
- Parameter rejecter: JS promise rejecter.
*/
@objc(getMaxTimeShift:resolver:rejecter:)
func getMaxTimeShift(
_ nativeId: NativeId,
resolver resolve: @escaping RCTPromiseResolveBlock,
rejecter reject: @escaping RCTPromiseRejectBlock
) {
bridge.uiManager.addUIBlock { [weak self] _, _ in
resolve(self?.players[nativeId]?.maxTimeShift)
}
}
}
8 changes: 8 additions & 0 deletions ios/RNPlayerView+PlayerListener.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ extension RNPlayerView: PlayerListener {
onSeeked?(event.toJSON())
}

func onTimeShift(_ event: TimeShiftEvent, player: Player) {
onTimeShift?(event.toJSON())
}

func onTimeShifted(_ event: TimeShiftedEvent, player: Player) {
onTimeShifted?(event.toJSON())
}

func onStallStarted(_ event: StallStartedEvent, player: Player) {
onStallStarted?(event.toJSON())
}
Expand Down
2 changes: 2 additions & 0 deletions ios/RNPlayerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ class RNPlayerView: UIView {
@objc var onPlaybackFinished: RCTBubblingEventBlock?
@objc var onSeek: RCTBubblingEventBlock?
@objc var onSeeked: RCTBubblingEventBlock?
@objc var onTimeShift: RCTBubblingEventBlock?
@objc var onTimeShifted: RCTBubblingEventBlock?
@objc var onStallStarted: RCTBubblingEventBlock?
@objc var onStallEnded: RCTBubblingEventBlock?
@objc var onTimeChanged: RCTBubblingEventBlock?
Expand Down
2 changes: 2 additions & 0 deletions ios/RNPlayerViewManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ @interface RCT_EXTERN_REMAP_MODULE(NativePlayerView, RNPlayerViewManager, RCTVie
RCT_EXPORT_VIEW_PROPERTY(onPlaybackFinished, RCTBubblingEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onSeek, RCTBubblingEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onSeeked, RCTBubblingEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onTimeShift, RCTBubblingEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onTimeShifted, RCTBubblingEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onStallStarted, RCTBubblingEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onStallEnded, RCTBubblingEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onTimeChanged, RCTBubblingEventBlock)
Expand Down
4 changes: 4 additions & 0 deletions src/components/PlayerView/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import {
ReadyEvent,
SeekedEvent,
SeekEvent,
TimeShiftEvent,
TimeShiftedEvent,
StallStartedEvent,
StallEndedEvent,
SourceErrorEvent,
Expand Down Expand Up @@ -89,6 +91,8 @@ interface EventProps {
onReady: ReadyEvent;
onSeek: SeekEvent;
onSeeked: SeekedEvent;
onTimeShift: TimeShiftEvent;
onTimeShifted: TimeShiftedEvent;
onStallStarted: StallStartedEvent;
onStallEnded: StallEndedEvent;
onSourceError: SourceErrorEvent;
Expand Down
2 changes: 2 additions & 0 deletions src/components/PlayerView/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,8 @@ export function PlayerView({
onReady={proxy(props.onReady)}
onSeek={proxy(props.onSeek)}
onSeeked={proxy(props.onSeeked)}
onTimeShift={proxy(props.onTimeShift)}
onTimeShifted={proxy(props.onTimeShifted)}
onStallStarted={proxy(props.onStallStarted)}
onStallEnded={proxy(props.onStallEnded)}
onSourceError={proxy(props.onSourceError)}
Expand Down
21 changes: 21 additions & 0 deletions src/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,27 @@ export interface SeekEvent extends Event {
*/
export interface SeekedEvent extends Event {}

/**
* Emitted when the player starts time shifting.
* Only applies to live streams.
*/
export interface TimeShiftEvent extends Event {
/**
* The position from which we start the time shift
*/
position: number;
/**
* The position to which we want to jump for the time shift
*/
targetPosition: number;
}

/**
* Emitted when time shifting has finished and data is available to continue playback.
* Only applies to live streams.
*/
export interface TimeShiftedEvent extends Event {}

/**
* Emitted when the player begins to stall and to buffer due to an empty buffer.
*/
Expand Down
31 changes: 30 additions & 1 deletion src/player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,19 @@ export class Player extends NativeInstance<PlayerConfig> {
PlayerModule.seek(this.nativeId, time);
};

/**
* Shifts the time to the given `offset` in seconds from the live edge. The resulting offset has to be within the
* timeShift window as specified by `maxTimeShift` (which is a negative value) and 0. When the provided `offset` is
* positive, it will be interpreted as a UNIX timestamp in seconds and converted to fit into the timeShift window.
* When the provided `offset` is negative, but lower than `maxTimeShift`, then it will be clamped to `maxTimeShift`.
* Has no effect for VoD.
*
* Has no effect if no sources are loaded.
*/
timeShift = (offset: number) => {
PlayerModule.timeShift(this.nativeId, offset);
};

/**
* Mutes the player if an audio track is available. Has no effect if the player is already muted.
*/
Expand Down Expand Up @@ -396,7 +409,23 @@ export class Player extends NativeInstance<PlayerConfig> {
* @returns `true` while an ad is being played back or when main content playback has been paused for ad playback.
* @platform iOS, Android
*/
isAd = (): Promise<boolean> => {
isAd = async (): Promise<boolean> => {
return PlayerModule.isAd(this.nativeId);
};

/**
* The current time shift of the live stream in seconds. This value is always 0 if the active `source` is not a
* live stream or no sources are loaded.
*/
getTimeShift = async (): Promise<number> => {
return PlayerModule.getTimeShift(this.nativeId);
};

/**
* The limit in seconds for time shifting. This value is either negative or 0 and it is always 0 if the active
* `source` is not a live stream or no sources are loaded.
*/
getMaxTimeShift = async (): Promise<number> => {
return PlayerModule.getMaxTimeShift(this.nativeId);
};
}