diff --git a/CHANGELOG.md b/CHANGELOG.md index dc969d91d..e7405a3fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] +### Added + +- Added support on iOS to push the initial state of the player from the iOS bridge to the React native adapter. + ### Fixed - Fixed an issue on iOS where the integrationParameters from the contentProtection section of the source were not processed correctly, resulting in failures for DRM connectors that depend on them. diff --git a/example/ios/ReactNativeTHEOplayer.xcodeproj/project.pbxproj b/example/ios/ReactNativeTHEOplayer.xcodeproj/project.pbxproj index 003d764dc..b91b08eb1 100644 --- a/example/ios/ReactNativeTHEOplayer.xcodeproj/project.pbxproj +++ b/example/ios/ReactNativeTHEOplayer.xcodeproj/project.pbxproj @@ -572,7 +572,7 @@ CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; - DEBUG_INFORMATION_FORMAT = dwarf; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_BITCODE = NO; ENABLE_TESTABILITY = YES; GCC_NO_COMMON_BLOCKS = YES; @@ -699,6 +699,8 @@ ); MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; + OTHER_CFLAGS = "$(inherited)"; + OTHER_CPLUSPLUSFLAGS = "$(inherited)"; OTHER_LDFLAGS = "$(inherited)"; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; @@ -766,6 +768,8 @@ "\"$(inherited)\"", ); MTL_ENABLE_DEBUG_INFO = NO; + OTHER_CFLAGS = "$(inherited)"; + OTHER_CPLUSPLUSFLAGS = "$(inherited)"; OTHER_LDFLAGS = "$(inherited)"; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; diff --git a/ios/THEOplayerRCTMainEventHandler.swift b/ios/THEOplayerRCTMainEventHandler.swift index 36a9aa75c..72042d212 100644 --- a/ios/THEOplayerRCTMainEventHandler.swift +++ b/ios/THEOplayerRCTMainEventHandler.swift @@ -7,7 +7,7 @@ public class THEOplayerRCTMainEventHandler { // MARK: Members private weak var player: THEOplayer? private weak var presentationModeContext: THEOplayerRCTPresentationModeContext? - private var loadedMetadataAndChapterTracksInfo: [[String:Any]] = [] + private(set) var loadedMetadataAndChapterTracksInfo: [[String:Any]] = [] // MARK: Events var onNativePlay: RCTDirectEventBlock? @@ -274,9 +274,9 @@ public class THEOplayerRCTMainEventHandler { self.loadedMetadataListener = player.addEventListener(type: PlayerEventTypes.LOADED_META_DATA) { [weak self, weak player] event in if DEBUG_THEOPLAYER_EVENTS { PrintUtils.printLog(logText: "[NATIVE] Received LOADED_META_DATA event from THEOplayer") } if let wplayer = player, - let welf = self, - let forwardedLoadedMetadataEvent = self?.onNativeLoadedMetadata { - let metadata = THEOplayerRCTTrackMetadataAggregator.aggregateTrackMetadata(player: wplayer, metadataTracksInfo: welf.loadedMetadataAndChapterTracksInfo) + let self, + let forwardedLoadedMetadataEvent = self.onNativeLoadedMetadata { + let metadata = THEOplayerRCTTrackMetadataAggregator.aggregateTrackInfo(player: wplayer, metadataTracksInfo: self.loadedMetadataAndChapterTracksInfo) forwardedLoadedMetadataEvent(metadata) } } diff --git a/ios/THEOplayerRCTPlayerStateBuilder.swift b/ios/THEOplayerRCTPlayerStateBuilder.swift new file mode 100644 index 000000000..daf618f11 --- /dev/null +++ b/ios/THEOplayerRCTPlayerStateBuilder.swift @@ -0,0 +1,100 @@ +import Foundation +import THEOplayerSDK + +let STATE_SOURCE = "source" +let STATE_CURRENT_TIME = "currentTime" +let STATE_CURRENT_PROGRAM_DATE_TIME = "currentProgramDateTime" +let STATE_PAUSED = "paused" +let STATE_PLAYBACK_RATE = "playbackRate" +let STATE_DURATION = "duration" +let STATE_VOLUME = "volume" +let STATE_MUTED = "muted" +let STATE_SEEKABLE = "seekable" +let STATE_BUFFERED = "buffered" +let STATE_START = "start" +let STATE_END = "end" + +class THEOplayerRCTPlayerStateBuilder { + private var playerState: [String: Any] = [:] + + func build() -> [String: Any] { + return playerState + } + + func source(_ sourceDescription: SourceDescription?) -> Self { + if let sourceDesc = sourceDescription { + playerState[STATE_SOURCE] = THEOplayerRCTSourceDescriptionAggregator.aggregateSourceDescription(sourceDescription: sourceDesc) + } + return self + } + + func currentTime(_ timeInSec: Double) -> Self { + playerState[STATE_CURRENT_TIME] = timeInSec * 1000 // sec -> msec + return self + } + + func currentProgramDateTime(_ programDataTime: Date?) -> Self { + if let date = programDataTime { + playerState[STATE_CURRENT_PROGRAM_DATE_TIME] = date.timeIntervalSince1970 * 1000 // sec -> msec + } + return self + } + + func paused(_ paused: Bool) -> Self { + playerState[STATE_PAUSED] = paused + return self + } + + func playbackRate(_ rate: Double) -> Self { + playerState[STATE_PLAYBACK_RATE] = rate + return self + } + + func duration(_ durationInSec: Double?) -> Self { + if let durationInSec = durationInSec { + playerState[PROP_DURATION] = THEOplayerRCTTypeUtils.encodeInfNan(durationInSec * 1000) // sec -> msec + } + return self + } + + func volume(_ volume: Float) -> Self { + playerState[STATE_VOLUME] = Double(volume) + return self + } + + func muted(_ muted: Bool) -> Self { + playerState[STATE_MUTED] = muted + return self + } + + func seekable(_ seekableRanges: [THEOplayerSDK.TimeRange]?) -> Self { + playerState[STATE_SEEKABLE] = fromTimeRanges(seekableRanges) + return self + } + + func buffered(_ bufferedRanges: [THEOplayerSDK.TimeRange]?) -> Self { + playerState[STATE_BUFFERED] = fromTimeRanges(bufferedRanges) + return self + } + + func trackInfo(_ trackInfo: [String: Any]) -> Self { + playerState.merge(trackInfo) { (current, _) in current } + return self + } + + private func fromTimeRanges(_ ranges: [THEOplayerSDK.TimeRange]?) -> [[String: Double]] { + guard let inRanges = ranges else { return [] } + + var outRanges: [[String:Double]] = [] + inRanges.forEach({ timeRange in + outRanges.append( + [ + STATE_START: timeRange.start * 1000, // sec -> msec + STATE_END: timeRange.end * 1000 // sec -> msec + ] + ) + }) + return outRanges + } +} + diff --git a/ios/THEOplayerRCTSourceDescriptionAggregator.swift b/ios/THEOplayerRCTSourceDescriptionAggregator.swift index 17dd2dd7a..fc9047fb6 100644 --- a/ios/THEOplayerRCTSourceDescriptionAggregator.swift +++ b/ios/THEOplayerRCTSourceDescriptionAggregator.swift @@ -4,24 +4,30 @@ import Foundation import THEOplayerSDK import UIKit -#if os(iOS) class THEOplayerRCTSourceDescriptionAggregator { - class func aggregateCacheTaskSourceDescription(sourceDescription: SourceDescription, cachingTaskId: String) -> [String:Any]? { + class func aggregateSourceDescription(sourceDescription: SourceDescription) -> [String:Any]? { do { let jsonEncoder = JSONEncoder() let data = try jsonEncoder.encode(sourceDescription) if let result = try? JSONSerialization.jsonObject(with: data, options: []) as? [String:Any] { - let srcDescription = THEOplayerRCTSourceDescriptionAggregator.sanitiseSourceDescriptionMetadata(input: result) - let extendedSrcDescription = THEOplayerRCTSourceDescriptionAggregator.addCachingTaskIdToMetadata(input: srcDescription, cachingTaskId: cachingTaskId) - return extendedSrcDescription + return THEOplayerRCTSourceDescriptionAggregator.sanitiseSourceDescriptionMetadata(input: result) } } catch { - if DEBUG { PrintUtils.printLog(logText: "[NATIVE] Could not aggregate sourceDescription for caching task: \(error.localizedDescription)")} + if DEBUG { PrintUtils.printLog(logText: "[NATIVE] Could not aggregate sourceDescription: \(error.localizedDescription)")} return nil } return nil } + class func aggregateCacheTaskSourceDescription(sourceDescription: SourceDescription, cachingTaskId: String) -> [String:Any]? { + if let result = THEOplayerRCTSourceDescriptionAggregator.aggregateSourceDescription(sourceDescription: sourceDescription) { + let srcDescription = THEOplayerRCTSourceDescriptionAggregator.sanitiseSourceDescriptionMetadata(input: result) + let extendedSrcDescription = THEOplayerRCTSourceDescriptionAggregator.addCachingTaskIdToMetadata(input: srcDescription, cachingTaskId: cachingTaskId) + return extendedSrcDescription + } + return nil + } + private class func sanitiseSourceDescriptionMetadata(input: [String:Any]) -> [String:Any] { var output: [String:Any] = input if let metadata = output[SD_PROP_METADATA] as? [String:Any], @@ -53,4 +59,3 @@ class THEOplayerRCTSourceDescriptionAggregator { return output } } -#endif diff --git a/ios/THEOplayerRCTTrackMetadataAggregator.swift b/ios/THEOplayerRCTTrackMetadataAggregator.swift index 2b1aff33e..8e0700e93 100644 --- a/ios/THEOplayerRCTTrackMetadataAggregator.swift +++ b/ios/THEOplayerRCTTrackMetadataAggregator.swift @@ -38,7 +38,7 @@ let PROP_END_ON_NEXT: String = "endOnNext" class THEOplayerRCTTrackMetadataAggregator { - class func aggregateTrackMetadata(player: THEOplayer, metadataTracksInfo: [[String:Any]]) -> [String:Any] { + class func aggregateTrackInfo(player: THEOplayer, metadataTracksInfo: [[String:Any]]) -> [String:Any] { let textTracks: TextTrackList = player.textTracks let audioTracks: AudioTrackList = player.audioTracks let videoTracks: VideoTrackList = player.videoTracks diff --git a/ios/THEOplayerRCTView.swift b/ios/THEOplayerRCTView.swift index 5705ee634..af19f263d 100644 --- a/ios/THEOplayerRCTView.swift +++ b/ios/THEOplayerRCTView.swift @@ -210,16 +210,43 @@ public class THEOplayerRCTView: UIView { } } - private func notifyNativePlayerReady() { + public func notifyNativePlayerReady() { DispatchQueue.main.async { let versionString = THEOplayer.version if let forwardedNativeReady = self.onNativePlayerReady { - forwardedNativeReady([ - "version": [ - "version" : versionString, - "playerSuiteVersion": versionString - ], - ]) + var payload: [String: Any] = [:] + + // pass initial player state + if let player = self.player { + // collect stored track metadata + let trackInfo = THEOplayerRCTTrackMetadataAggregator.aggregateTrackInfo( + player: player, + metadataTracksInfo: self.mainEventHandler.loadedMetadataAndChapterTracksInfo + ) + + // build state + payload["state"] = THEOplayerRCTPlayerStateBuilder() + .source(player.source) + .currentTime(player.currentTime) + .currentProgramDateTime(player.currentProgramDateTime) + .paused(player.paused) + .playbackRate(player.playbackRate) + .duration(player.duration) + .volume(player.volume) + .muted(player.muted) + .seekable(player.seekable) + .buffered(player.buffered) + .trackInfo(trackInfo) + .build() + } + + // pass version onfo + payload["version"] = [ + "version": versionString, + "playerSuiteVersion": versionString + ] + + forwardedNativeReady(payload) } } }