Phenix Mobile SDK v2020.1.75 Pre-release Notes
Release Date: 2020.12.11
Features/Improvements
PhenixTimeShiftOptions
viaPhenixRendererOptions
allows customization of rendition selection behavior
Issues addressed in this release:
Fixed possible crash due race condition occurring when shutting down a renderer
Fixed resource leaks associated with time-shifts
To get the Android SDK:
Update the Phenix SDK version in build.gradle file to `2020.1.75`
To get the iOS SDK:
Update the Phenix SDK version in Podfile to `2020.1.75-beta`
Notes on Integrating the Android SDK
import com.phenixrts.common.Disposable;
import com.phenixrts.pcast.Renderer;
import com.phenixrts.pcast.TimeShift;
import java.util.Date;
// Renderer has been obtained from `MediaStream` or `ExpressSubscriber`:
var renderer: Renderer = ...
if (!renderer.isSeekable) {
// This stream does not support time shifting
return;
}
// Must be in UTC:
var timePoint = Date(...)
var timeShift = renderer.seek(timePoint)
// Observe ready status and call `loop` once TimeShift is ready.
// This ensures that when `loop` is called playback will start
// instantly.
// All observables return a `Disposable` object. This needs to be stored
// in order to keep the observable subscription alive.
// It is recommended to make this a member of the enclosing class
var disposables = mutableListOf<Disposable>()
timeShift.observableReadyForPlaybackStatus.subscribe { isReady ->
if (isReady) {
var loopDurationInMilliseconds = 30000L
timeShift.loop(loopDurationInMilliseconds)
}
}.run{ disposables.add(this) }
// When time shifted viewing is no longer desired:
// Dispose the `TimeShift` object. It will no longer be useable
// after this operation. A new instance can be obtained via `seek`
timeShift.dispose() // <-- Playback will jump back to real-time head
// It is also possible to stop a timeshift in progress so it can
// be used again by calling `loop` later
timeShift.stop() // <-- Playback will jump back to real-time head
// After `stop()` the ready status will (briefly) switch to false, then
// back to true when enough data has been re-cached.
// `TimeShift` objects can fail, either during creating when passing in
// an invalid timestamp to `Renderer.seek()` or any of the other `TimeShift`
// method. Once a `TimeShift` has failed, it should be disposed and a new
// one created.
timeShift.observableFailure.subscribe { status ->
// a non-OK `RequestStatus` is returned here
}.run { disposables.add(this) }
// Pause playback
timeShift.pause()
// Resume playback after pause
timeShift.play()
// The current playback head can be observed. Each video frame will trigger
// the observable. The head is reported as a `Date` object and represents UTC.
timeShift.observablePlaybackHead.subscribe { playbackHead ->
// a `Date` object. Avoid blocking this thread and dispatch UI updates
// as needed.
}.run { disposables.add(this) }
// `TimeShift` supports seeking to any location within the current segment.
// The segment is defined by:
// Start: Time point passed into `Renderer.seek`
// End : Start plus duration passed into `TimeShift.loop`
// Seeking is only possible if the `TimeShift` is either stopped or paused.
// Once `TimeShift` is ready for playback, the observable returned by `seek`
// will return a status code. Call `play()` to start/resume playback if the
// status is OK.
var relativeSeekTimeInMillis = -10000L // Seeks back 10 seconds
timeShift.seek(relativeSeekTimeInMillis).subscribe { status ->
if (status == RequestStatus.OK) {
timeShift.play()
} else {
// Handle error. In most cases the seek argument would have led to
// a location outside of the segment
}
}.run { disposables.add(this) }
// There is also a second version of `seek` available, which accepts a `Date`
// as its argument representing an absolute time point in UTC (also has to
// fall within the boundaries of the segment).
// A `TimeShift` stream generally consists of several so called layers. Layers
// have different bitrates, usually by varying resolution and framerate (lower
// bitrate layers tend to have lower resolutions and/or framerates).
// While a `TimeShift` is playing back content, it will automatically select
// an appropriate layer based on the pixel resolution of the rendering
// surface. The logic attempts to select a layer, whose resolution matches
// that of the rendering surface as closely as possible.
// Sometimes an app may want to further restrict the playback layer in order
// to reduce battery usage for instance. This is possible via the
// `TimeShift.limitBandwidth` API. It forces the `TimeShift` to only select
// layers with an average bitrate less or equal to the value passed into
// `limitBandwidth`.
// The limitation will remain in effect for as long as the app holds on to
// the `Disposable` object returned by the API.
// This API can be called at any time: before and during playback.
var bandwithLimitInBps = 400000L
var limitBandwidthDisposable = timeShift?.limitBandwidth(bandwithLimitInBps)
// Call `dispose()` on the `Disposable` object to release the bandwidth limit
limitbandwidthDisposable?.dispose()
// Certain behaviors of `TimeShift` can be changed by passing in
// custom options when the `Renderer` is created:
var rendererOptions = RendererOptions()
// When calling `Renderer.seek` or `TimeShift.seek` the
// bandwidth will be automatically limited to shorten the amount of time
// it takes for the `TimeShift` object to become ready.
// The downside of this approach is that once the playback starts, it will
// do so on a lower bitrate rendition of the stream and it generally takes a
// few seconds before the rendition switches to a higher bitrate.
// This behavior is enabled by default, but can be disabled by setting the
// following option to false:
rendererOptions.timeShiftOptions.lowerBandwidthWhileSeekingEnabled = false
// The size of the viewport will determine which rendition of the stream is
// selected (the rendition whose height in pixels is closest to the current
// height of the viewport). This selection process is continuous, which means
// that as the viewport size changes, so does the selected rendition.
// After a new rendition has been selected, it will not render frames
// instantly. Instead, it will take up to about 5 seconds before it will
// render frames.
// This behavior is enabled by default, but can be disabled by setting the
// following option to false:
rendererOptions.timeShiftOptions.viewportSizeBasedRenditionSelectionEnabled
= false
// The options object can be passed in directly to the `createRenderer`
// method:
var subscriber: ExpressSubscriber = ... // previously obtained
var renderer = subscriber.createRenderer(rendererOptions)
// Alternatively, it can be passed via various options builders, for example
// for `joinChannel`:
var joinChannelOptionsBuilder =
ChannelExpressFactory.createJoinChannelOptionsBuilder()
joinChannelOptionsBuilder.withRendererOptions(rendererOptions)
Notes on Integrating the iOS SDK
import PhenixSdk
// Renderer has been obtained from `PhenixMediaStream` or
// `PhenixExpressSubscriber`:
var renderer: PhenixRenderer = ...
if (!renderer.isSeekable) {
// This stream does not support time shifting
return;
}
// Must be in UTC:
let timePoint = Date(...)
var timeShift = renderer.seek(timePoint)
// Observe ready status and call `loop` once PhenixTimeShift is ready.
// This ensures that when `loop` is called playback will start
// instantly.
// All observables return a `PhenixDisposable` object. This needs to be stored
// in order to keep the observable subscription alive.
// It is recommended to make this a member of the enclosing class
let readyDisposable = timeShift?.
getObservableReadyForPlaybackStatus()?.subscribe { (isReadyChange) in
if (isReadyChange?.value.boolValue) {
let loopDuration = TimeInterval(30.0)
timeShift?.loop(loopDuration)
}
}
// When time shifted viewing is no longer desired:
// Dispose the `PhenixTimeShift` object.
timeShift = nil // <-- Playback will jump back to real-time head
// It is also possible to stop a timeshift in progress so it can
// be used again by calling `loop` later
timeShift?.stop() // <-- Playback will jump back to real-time head
// After `stop()` the ready status will (briefly) switch to false, then
// back to true when enough data has been re-cached.
// `PhenixTimeShift` objects can fail, either during creating when passing in
// an invalid timestamp to `PhenixRenderer.seek()` or any of the other
// `PhenixTimeShift` method. Once a `PhenixTimeShift` has failed, it should be
// dropped and a new one created.
let failureDisposable = timeShift?.
getObservableFailure()?.subscribe { (failureChange) in
let errorStatus: PhenixRequestStatus = (failureChange?.value.status)!
// a non-ok `PhenixRequestStatus` is returned here
}
// Pause playback
timeShift?.pause()
// Resume playback after pause
timeShift?.play()
// The current playback head can be observed. Each video frame will trigger
// the observable. The head is reported as a `NSDate` object and represents
// UTC.
let playbackHeadDisposable = timeShift?.
getObservablePlaybackHead()?.subscribe { (playbackHeadChange) in
let currentPlaybackHead: NSDate = (playbackHeadChange?.value!)!
// Avoid blocking this thread and dispatch UI updates as needed
}
// `PhenixTimeShift` supports seeking to any location within the current
// segment.
// The segment is defined by:
// Start: Time point passed into `PhenixRenderer.seek`
// End : Start plus duration passed into `PhenixTimeShift.loop`
// Seeking is only possible if the `PhenixTimeShift` is either stopped or
// paused.
// Once `PhenixTimeShift` is ready for playback, the observable returned by
// `seek` will return a status code. Call `play()` to start/resume playback if
// the status is OK.
let relativeSeekTime: TimeInterval = -10.0 // Seeks back 10 seconds
let seekDisposable = timeShift?.
seekRelative(relativeSeekTime)?.subscribe { (seekChange) in
let status: PhenixRequestStatus = (seekChange?.value.status)!
if (status == .ok) {
timeShift?.play()
} else {
// Handle error. In most cases the seek argument would have
// led to a location outside of the segment
}
}
// There is also a second version of seek available named `seekAbsolute`,
// which accepts a `Date` as its argument representing an absolute time point
// in UTC (also has to fall within the boundaries of the segment).
// A `PhenixTimeShift` stream generally consists of several so called layers.
// Layers have different bitrates, usually by varying resolution and framerate
// (lower bitrate layers tend to have lower resolutions and/or framerates).
// While a `PhenixTimeShift` is playing back content, it will automatically
// select an appropriate layer based on the pixel resolution of the rendering
// layer. The logic attempts to select a layer, whose resolution matches
// that of the rendering layer as closely as possible.
// Sometimes an app may want to further restrict the playback layer in order
// to reduce battery usage for instance. This is possible via the
// `PhenixTimeShift.limitBandwidth` API. It forces the `PhenixTimeShift` to
// only select layers with an average bitrate less or equal to the value
// passed into `limitBandwidth`.
// The limitation will remain in effect for as long as the app holds on to
// the `PhenixDisposable` object returned by the API.
// This API can be called at any time: before and during playback.
let bandwithLimitInBps: UInt64 = 400000
var limitBandwidthDisposable = timeShift?.limitBandwidth(bandwithLimitInBps)
// Set the `PhenixDisposable` object to nil to release the bandwidth limit
limitBandwidthDisposable = nil
// Certain behaviors of `PhenixTimeShift` can be changed by passing in
// custom options when the `PhenixRenderer` is created:
let rendererOptions = PhenixRendererOptions()
// When calling `PhenixRenderer.seek` or `PhenixTimeShift.seek` the
// bandwidth will be automatically limited to shorten the amount of time
// it takes for the `PhenixTimeShift` object to become ready.
// The downside of this approach is that once the playback starts, it will
// do so on a lower bitrate rendition of the stream and it generally takes a
// few seconds before the rendition switches to a higher bitrate.
// This behavior is enabled by default, but can be disabled by setting the
// following option to false:
rendererOptions.timeShiftOptions.lowerBandwidthWhileSeekingEnabled = false
// The size of the viewport will determine which rendition of the stream is
// selected (the rendition whose height in pixels is closest to the current
// height of the viewport). This selection process is continuous, which means
// that as the viewport size changes, so does the selected rendition.
// After a new rendition has been selected, it will not render frames
// instantly. Instead, it will take up to about 5 seconds before it will
// render frames.
// This behavior is enabled by default, but can be disabled by setting the
// following option to false:
rendererOptions.timeShiftOptions.viewportSizeBasedRenditionSelectionEnabled
= false
// The options object can be passed in directly to the `createRenderer`
// method:
var subscriber: PhenixExpressSubscriber = ... // previously obtained
let renderer = subscriber.createRenderer(rendererOptions)
// Alternatively, it can be passed via various options builders, for example
// for `joinChannel`:
let joinChannelOptionsBuilder =
PhenixChannelExpressFactory.createJoinChannelOptionsBuilder()
joinChannelOptionsBuilder.withRendererOptions(rendererOptions)
©2020-2021 Phenix Real Time Solutions, Inc.