Phenix Mobile SDK v2020.1.75 Pre-release Notes

Release Date: 2020.12.11

Features/Improvements

  • PhenixTimeShiftOptions via PhenixRendererOptions 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.