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

Release/v8.12.0 #475

Merged
merged 33 commits into from
Jan 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
2ebb629
Properly support passing an array of events for a listener
tvanlaerhoven Dec 17, 2024
f72c658
Don't keep strong reference to targetViews
wvanhaevre Dec 18, 2024
8c87a77
Add changelog
wvanhaevre Dec 18, 2024
c628186
Allow configuration of ad load timeout for Google IMA plugin
tch-vidangel Dec 19, 2024
137ef70
Rework BackgroundAudioConfig delegate flow to weak view usage
wvanhaevre Dec 19, 2024
ce94755
Rework PipConfig delegation flow to weak view usage
wvanhaevre Dec 19, 2024
031bb3c
Drop ios destroy method bridging
wvanhaevre Dec 19, 2024
d454bdf
Drop destroy method on THEOplayer interface and facade for mobile
wvanhaevre Dec 19, 2024
64ead72
Trigger ios native player destruction from THEOplayerView deinit
wvanhaevre Dec 19, 2024
0a261b1
Revert ViewController wrapping that keeps strong reference to THEOpla…
wvanhaevre Dec 19, 2024
b6a0765
Add changelog
wvanhaevre Dec 19, 2024
08ff785
Extract backgroundAudio management to separate class
wvanhaevre Dec 19, 2024
bf26bd6
Call missing destroy for PresentationModeManager
wvanhaevre Dec 19, 2024
6c1767e
Add interruption config to example app
wvanhaevre Dec 19, 2024
121883a
Extract pip delegate handling to separate class
wvanhaevre Dec 19, 2024
d023644
Merge pull request #469 from THEOplayer/bugfix/ios-presentationMode-leak
tvanlaerhoven Dec 23, 2024
f49d999
Merge pull request #463 from THEOplayer/feature/eventlistener_array
tvanlaerhoven Dec 23, 2024
597e611
Merge pull request #466 from THEOplayer/bugfix/ios-drop-destroyPlayer…
tvanlaerhoven Dec 23, 2024
f88d960
Fix native track conversion
tvanlaerhoven Dec 23, 2024
350b646
Add changelog entry
tvanlaerhoven Dec 23, 2024
06546ef
Merge pull request #471 from THEOplayer/bugfix/texttrack_with_initial…
tvanlaerhoven Dec 23, 2024
2c8fa2d
Fix iOS to work on a time interval rather than an int
tch-vidangel Jan 6, 2025
021425e
Do not warn about missing cast classes when not using cast integration
tvanlaerhoven Jan 8, 2025
96dbe9d
Add an R8 rule for any source classes instantiated by gson
tvanlaerhoven Jan 8, 2025
fba39e2
Add changelog entry
tvanlaerhoven Jan 8, 2025
544c915
Prevent java.lang.NullPointerException crash
georgechoustoulakis Jan 8, 2025
95997c9
Add changelog
georgechoustoulakis Jan 8, 2025
a7fef11
Merge pull request #473 from THEOplayer/bugfix/android-proguard-cast
tvanlaerhoven Jan 9, 2025
c0c0eae
Merge pull request #468 from tch-vidangel/feature/all_configure_ima_a…
tvanlaerhoven Jan 9, 2025
cb37f42
Merge branch 'develop' into bugfix/fix-rare-nullpointer-exception
tvanlaerhoven Jan 9, 2025
f78b623
Merge pull request #474 from THEOplayer/bugfix/fix-rare-nullpointer-e…
tvanlaerhoven Jan 9, 2025
cfc833e
Update changelog
tvanlaerhoven Jan 9, 2025
e702b46
8.12.0
tvanlaerhoven Jan 9, 2025
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
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.1.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## [8.12.0] - 25-01-09

### Fixed

- Fixed a memory leak on iOS, where the presentationModeManager was holding a strong reference to the fullscreen's target and return views
- Fixed an issue on iOS where the destruction of the THEOplayerView was not always propagated correctly over the iOS Bridge, resulting in an occasional memory leak.
- Fixed an issue where, when requesting a text track's cues, the time properties would sometimes be in seconds instead of milliseconds.
- Fixed a rare crash on Android due to a `java.lang.NullPointerException` when creating the THEOplayerView.
- Fixed an issue on Android where R8 minification would obfuscate some API class names, which could lead to a crash.

### Added

- Added a `adLoadTimeout` property to `GoogleImaConfiguration` to control the amount of time that the SDK will wait before moving onto the next ad or main content.

## [8.11.1] - 24-12-18

### Fixed
Expand Down
4 changes: 4 additions & 0 deletions android/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# Do no warn if any of the API classes we resolve with compileOnly are missing because the feature
# is disabled: it is expected.
-dontwarn com.theoplayer.android.api.**
-dontwarn com.google.android.gms.cast.**

# We rely on gson to instantiate some source classes from json, so make sure they are not obfuscated.
-keep,includedescriptorclasses class com.theoplayer.android.api.source.** { *; }
9 changes: 8 additions & 1 deletion android/src/main/java/com/theoplayer/PlayerConfigAdapter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ private const val PROP_RETRY_MAX_BACKOFF = "maximumBackoff"
private const val PROP_CAST_CONFIGURATION = "cast"
private const val PROP_ADS_CONFIGURATION = "ads"
private const val PROP_IMA_CONFIGURATION = "ima"
private const val PROP_IMA_AD_LOAD_TIMEOUT = "adLoadTimeout"
private const val PROP_MEDIA_CONTROL = "mediaControl"
private const val PROP_PPID = "ppid"
private const val PROP_MAX_REDIRECTS = "maxRedirects"
Expand Down Expand Up @@ -169,11 +170,17 @@ class PlayerConfigAdapter(private val configProps: ReadableMap?) {
}
}
}
// bitrate is configured under the ima config
// bitrate and timeout are configured under the ima config
configProps?.getMap(PROP_ADS_CONFIGURATION)?.getMap(PROP_IMA_CONFIGURATION)?.run {
if (hasKey(PROP_BITRATE)) {
bitrateKbps = getInt(PROP_BITRATE)
}

// The time needs to be in milliseconds on android but seconds on ios.
// we unify the prop from javascript by multiplying it by 1000 here
if (hasKey(PROP_IMA_AD_LOAD_TIMEOUT)) {
setLoadVideoTimeout(getInt(PROP_IMA_AD_LOAD_TIMEOUT) * 1000)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ class ReactTHEOplayerContext private constructor(
}

private fun initializePlayerView() {
playerView = object : THEOplayerView(reactContext.currentActivity!!, configAdapter.playerConfig()) {
playerView = object : THEOplayerView(reactContext, configAdapter.playerConfig()) {
private fun measureAndLayout() {
measure(
MeasureSpec.makeMeasureSpec(measuredWidth, MeasureSpec.EXACTLY),
Expand Down
2 changes: 1 addition & 1 deletion example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export default function App() {

player.source = SOURCES[0].source;

player.backgroundAudioConfiguration = { enabled: true };
player.backgroundAudioConfiguration = { enabled: true, shouldResumeAfterInterruption: true };
player.pipConfiguration = { startsAutomatically: true };

console.log('THEOplayer is ready');
Expand Down
2 changes: 0 additions & 2 deletions ios/THEOplayerRCTBridge.m
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,6 @@ @interface RCT_EXTERN_REMAP_MODULE(THEORCTPlayerModule, THEOplayerRCTPlayerAPI,
RCT_EXTERN_METHOD(setTextTrackStyle:(nonnull NSNumber *)node
textTrackStyle:(NSDictionary)textTrackStyle)

RCT_EXTERN_METHOD(destroyPlayer:(nonnull NSNumber *)node);

@end

// ----------------------------------------------------------------------------
Expand Down
10 changes: 0 additions & 10 deletions ios/THEOplayerRCTPlayerAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -356,14 +356,4 @@ class THEOplayerRCTPlayerAPI: NSObject, RCTBridgeModule {
}
}
}

@objc(destroyPlayer:)
func destroyPlayer(_ node: NSNumber) -> Void {
DispatchQueue.main.async {
if let theView = self.bridge.uiManager.view(forReactTag: node) as? THEOplayerRCTView {
theView.destroyPlayer()
}
}
}

}
63 changes: 29 additions & 34 deletions ios/THEOplayerRCTView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,16 @@ public class THEOplayerRCTView: UIView {
public private(set) var player: THEOplayer?
public private(set) var mainEventHandler: THEOplayerRCTMainEventHandler
public private(set) var broadcastEventHandler: THEOplayerRCTBroadcastEventHandler
let theoPlayerViewController = UIViewController()
var textTrackEventHandler: THEOplayerRCTTextTrackEventHandler
var mediaTrackEventHandler: THEOplayerRCTMediaTrackEventHandler
var metadataTrackEventHandler: THEOplayerRCTSideloadedMetadataTrackEventHandler
var adEventHandler: THEOplayerRCTAdsEventHandler
var castEventHandler: THEOplayerRCTCastEventHandler
var presentationModeManager: THEOplayerRCTPresentationModeManager
var backgroundAudioManager: THEOplayerRCTBackgroundAudioManager
var nowPlayingManager: THEOplayerRCTNowPlayingManager
var remoteCommandsManager: THEOplayerRCTRemoteCommandsManager
var pipManager: THEOplayerRCTPipManager
var pipControlsManager: THEOplayerRCTPipControlsManager

var adsConfig = AdsConfig()
Expand All @@ -36,8 +37,8 @@ public class THEOplayerRCTView: UIView {
}
var backgroundAudioConfig = BackgroundAudioConfig() {
didSet {
self.updateInterruptionNotifications()
self.updateAVAudioSessionMode()
self.backgroundAudioManager.updateInterruptionNotifications()
self.backgroundAudioManager.updateAVAudioSessionMode()
}
}

Expand All @@ -61,8 +62,10 @@ public class THEOplayerRCTView: UIView {
self.adEventHandler = THEOplayerRCTAdsEventHandler()
self.castEventHandler = THEOplayerRCTCastEventHandler()
self.presentationModeManager = THEOplayerRCTPresentationModeManager()
self.backgroundAudioManager = THEOplayerRCTBackgroundAudioManager()
self.nowPlayingManager = THEOplayerRCTNowPlayingManager()
self.remoteCommandsManager = THEOplayerRCTRemoteCommandsManager()
self.pipManager = THEOplayerRCTPipManager()
self.pipControlsManager = THEOplayerRCTPipControlsManager()

super.init(frame: .zero)
Expand All @@ -71,20 +74,32 @@ public class THEOplayerRCTView: UIView {
required init?(coder aDecoder: NSCoder) {
fatalError("[NATIVE] init(coder:) has not been implemented")
}

deinit {
self.mainEventHandler.destroy()
self.textTrackEventHandler.destroy()
self.mediaTrackEventHandler.destroy()
self.adEventHandler.destroy()
self.castEventHandler.destroy()
self.nowPlayingManager.destroy()
self.remoteCommandsManager.destroy()
self.pipManager.destroy()
self.pipControlsManager.destroy()
self.presentationModeManager.destroy()
self.backgroundAudioManager.destroy()

self.destroyBackgroundAudio()
self.player?.removeAllIntegrations()
self.player?.destroy()
self.player = nil
if DEBUG_THEOPLAYER_INTERACTION { PrintUtils.printLog(logText: "[NATIVE] THEOplayer instance destroyed.") }
}

override public func layoutSubviews() {
super.layoutSubviews()
if let player = self.player {
player.frame = self.frame
player.autoresizingMask = [.flexibleBottomMargin, .flexibleHeight, .flexibleLeftMargin, .flexibleRightMargin, .flexibleTopMargin, .flexibleWidth]

// wrap theoPlayerViewController around the view
if theoPlayerViewController.parent == nil,
let parentViewController = self.findViewController() {
parentViewController.addChild(self.theoPlayerViewController)
self.theoPlayerViewController.didMove(toParent: parentViewController)
self.theoPlayerViewController.view = self
}
}
}

Expand All @@ -97,12 +112,14 @@ public class THEOplayerRCTView: UIView {
self.mainEventHandler.setPlayer(player)
self.textTrackEventHandler.setPlayer(player)
self.mediaTrackEventHandler.setPlayer(player)
self.presentationModeManager.setPlayer(player, view: self)
self.adEventHandler.setPlayer(player)
self.castEventHandler.setPlayer(player)
self.nowPlayingManager.setPlayer(player)
self.remoteCommandsManager.setPlayer(player)
self.pipControlsManager.setPlayer(player)
self.presentationModeManager.setPlayer(player, view: self)
self.backgroundAudioManager.setPlayer(player, view: self)
self.pipManager.setView(view: self)
// Attach player to view
player.addAsSubview(of: self)
}
Expand Down Expand Up @@ -137,28 +154,6 @@ public class THEOplayerRCTView: UIView {
return self.player
}

// MARK: - Destroy Player

public func destroyPlayer() {
self.mainEventHandler.destroy()
self.textTrackEventHandler.destroy()
self.mediaTrackEventHandler.destroy()
self.adEventHandler.destroy()
self.castEventHandler.destroy()
self.nowPlayingManager.destroy()
self.remoteCommandsManager.destroy()
self.pipControlsManager.destroy()

self.destroyBackgroundAudio()
self.player?.removeAllIntegrations()
self.player?.destroy()
self.player = nil
if DEBUG_THEOPLAYER_INTERACTION { PrintUtils.printLog(logText: "[NATIVE] THEOplayer instance destroyed.") }

self.theoPlayerViewController.view = nil
self.theoPlayerViewController.removeFromParent()
}

func processMetadataTracks(metadataTrackDescriptions: [TextTrackDescription]?) {
THEOplayerRCTSideloadedMetadataProcessor.loadTrackInfoFromTrackDescriptions(metadataTrackDescriptions) { tracksInfo in
self.mainEventHandler.setLoadedMetadataTracksInfo(tracksInfo)
Expand Down
9 changes: 0 additions & 9 deletions ios/THEOplayerRCTViewManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,4 @@ class THEOplayerRCTViewManager: RCTViewManager {
override class func requiresMainQueueSetup() -> Bool {
return true
}

@objc func destroy(_ node: NSNumber) {
DispatchQueue.main.async {
let theView = self.bridge.uiManager.view(
forReactTag: node
) as! THEOplayerRCTView
theView.destroyPlayer()
}
}
}
4 changes: 4 additions & 0 deletions ios/ads/THEOplayerRCTView+Ads.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ extension THEOplayerRCTView {
imaRenderSettings.mimeTypes = allowedMimeTypes
}

if let adLoadTimeout = self.adsConfig.adsImaConfig.adLoadTimeout {
imaRenderSettings.loadVideoTimeout = adLoadTimeout
}

// setup integration
let imaIntegration = GoogleIMAIntegrationFactory.createIntegration(on: player, with: imaSettings)
imaIntegration.renderingSettings = imaRenderSettings
Expand Down
7 changes: 6 additions & 1 deletion ios/ads/THEOplayerRCTView+AdsConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,16 @@ struct AdsImaConfig {
var autoPlayAdBreaks: Bool?
var sessionID: String?
var bitrate: Int
var adLoadTimeout: TimeInterval?

init(maxRedirects: UInt, enableDebugMode: Bool, ppid: String? = nil, featureFlags: [String : String]? = nil, autoPlayAdBreaks: Bool? = nil, sessionID: String? = nil, bitrate: Int? = -1) {
init(maxRedirects: UInt, enableDebugMode: Bool, ppid: String? = nil, featureFlags: [String : String]? = nil, autoPlayAdBreaks: Bool? = nil, sessionID: String? = nil, bitrate: Int? = -1, adLoadTimeout: TimeInterval? = nil) {
self.maxRedirects = maxRedirects
self.enableDebugMode = enableDebugMode
self.ppid = ppid
self.featureFlags = featureFlags
self.autoPlayAdBreaks = autoPlayAdBreaks
self.sessionID = sessionID
self.adLoadTimeout = adLoadTimeout
#if canImport(THEOplayerGoogleIMAIntegration)
self.bitrate = bitrate ?? kIMAAutodetectBitrate
#else
Expand Down Expand Up @@ -67,6 +69,9 @@ extension THEOplayerRCTView {
if let bitrate = adsImaConfig["bitrate"] as? Int {
self.adsConfig.adsImaConfig.bitrate = bitrate
}
if let adLoadTimeout = adsImaConfig["adLoadTimeout"] as? TimeInterval {
self.adsConfig.adsImaConfig.adLoadTimeout = adLoadTimeout
}
}
}
}
Expand Down
105 changes: 105 additions & 0 deletions ios/backgroundAudio/THEOplayerRCTBackgroundAudioManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// TTHEOplayerRCTBackgroundAudioManager.swift

import Foundation
import THEOplayerSDK
import AVFAudio
import AVKit

struct BackgroundAudioConfig {
var enabled: Bool = false
var shouldResumeAfterInterruption: Bool = false
var audioSessionMode: AVAudioSession.Mode = .moviePlayback
}

class THEOplayerRCTBackgroundAudioManager: NSObject, BackgroundPlaybackDelegate {
// MARK: Members
private weak var player: THEOplayer?
private weak var view: THEOplayerRCTView?

// MARK: - player setup / breakdown
func setPlayer(_ player: THEOplayer, view: THEOplayerRCTView?) {
self.player = player
self.view = view
}

// MARK: - destruction
func destroy() {
self.cancelInterruptionNotifications()
}

// MARK: - logic
func shouldContinueAudioPlaybackInBackground() -> Bool {
if let view = self.view {
view.nowPlayingManager.updateNowPlaying()
return view.backgroundAudioConfig.enabled
}
return false
}

func cancelInterruptionNotifications() {
NotificationCenter.default.removeObserver(self,
name: AVAudioSession.interruptionNotification,
object: AVAudioSession.sharedInstance())
}

func updateInterruptionNotifications() {
guard let view = self.view else { return }

// Get the default notification center instance.
if view.backgroundAudioConfig.shouldResumeAfterInterruption {
NotificationCenter.default.addObserver(self,
selector: #selector(handleInterruption),
name: AVAudioSession.interruptionNotification,
object: AVAudioSession.sharedInstance())
} else {
NotificationCenter.default.removeObserver(self,
name: AVAudioSession.interruptionNotification,
object: AVAudioSession.sharedInstance())
}
}

func updateAVAudioSessionMode() {
guard let view = self.view else { return }

do {
THEOplayer.automaticallyManageAudioSession = (view.backgroundAudioConfig.audioSessionMode == .moviePlayback)
try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback, mode: view.backgroundAudioConfig.audioSessionMode)
if view.backgroundAudioConfig.audioSessionMode != .moviePlayback {
if DEBUG_INTERRUPTIONS { PrintUtils.printLog(logText: "[NATIVE] AVAudioSession mode updated to \(view.backgroundAudioConfig.audioSessionMode.rawValue)") }
}
} catch {
if DEBUG_INTERRUPTIONS { PrintUtils.printLog(logText: "[NATIVE] Unable to update AVAudioSession mode to \(view.backgroundAudioConfig.audioSessionMode.rawValue): \(error)") }
}
}

@objc func handleInterruption(notification: Notification) {
guard let userInfo = notification.userInfo,
let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
let type = AVAudioSession.InterruptionType(rawValue: typeValue) else {
return
}

// Switch over the interruption type.
switch type {
case .began:
// An interruption began. Update the UI as necessary.
if DEBUG_INTERRUPTIONS { PrintUtils.printLog(logText: "[NATIVE] An interruption began")}
case .ended:
// An interruption ended. Resume playback, if appropriate.
if DEBUG_INTERRUPTIONS { PrintUtils.printLog(logText: "[NATIVE] An interruption ended")}
guard let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt else { return }
let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
if options.contains(.shouldResume) {
// An interruption ended. Resume playback.
if let player = self.player {
if DEBUG_INTERRUPTIONS { PrintUtils.printLog(logText: "[NATIVE] Ended interruption should resume playback => play()")}
player.play()
}
} else {
// An interruption ended. Don't resume playback.
if DEBUG_INTERRUPTIONS { PrintUtils.printLog(logText: "[NATIVE] Ended interruption should not resume playback.")}
}
default: ()
}
}
}
Loading
Loading