diff --git a/.github/workflows/pr_ios.yml b/.github/workflows/pr_ios.yml index c731e21d7..c4cc5d867 100644 --- a/.github/workflows/pr_ios.yml +++ b/.github/workflows/pr_ios.yml @@ -8,8 +8,8 @@ jobs: build: strategy: matrix: - xcode_version: [ '15.2' ] - runs-on: macos-14 + xcode_version: [ '15.4.0' ] + runs-on: macos-latest steps: - name: Checkout repository uses: actions/checkout@v4 @@ -17,7 +17,7 @@ jobs: - name: Select Xcode ${{ matrix.xcode_version }} uses: maxim-lobanov/setup-xcode@v1 with: - xcode-version: ${{ matrix.xcode_version }} # Check versions: https://github.com/actions/runner-images/blob/main/images/macos/macos-14-Readme.md + xcode-version: ${{ matrix.xcode_version }} - name: Setup Node uses: actions/setup-node@v4 @@ -50,10 +50,10 @@ jobs: pod update - name: Start iOS simulator - uses: futureware-tech/simulator-action@v3 + uses: futureware-tech/simulator-action@v4 with: - model: 'iPhone 14' - os_version: '>=16.0' + model: 'iPhone 15' + os_version: '>=14.0' - name: Run e2e tests working-directory: e2e diff --git a/CHANGELOG.md b/CHANGELOG.md index 5feb05c0c..79216273a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,18 @@ 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.8.0] - 24-11-19 + +### Added + +- Added `HomeIndicatorViewController` to iOS, which can be used as an alternative `rootViewController` for the native App. It will automatically show/hide the home indicator when transitioning from/to fullscreen presentationMode. + +### Changed + +- Simplified the `viewController` reparenting mechanism on iOS that is applied when changing the presentationMode to/from fullscreen. +- The `MediaPlaybackService` on Android is never restarted if a MediaButton event is received after the app was closed. +- Added a consumer R8 config file on Android, telling R8 not to throw errors or warnings because of classes that are expected to be missing. + ## [8.7.0] - 24-11-05 ### Fixed diff --git a/android/build.gradle b/android/build.gradle index 787efecfb..356ee5eb7 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -66,6 +66,8 @@ android { buildConfigField "boolean", "EXTENSION_ADS", "${enabledAds}" buildConfigField "boolean", "EXTENSION_CAST", "${enabledCast}" buildConfigField "boolean", "EXTENSION_MEDIASESSION", "${enabledMediaSession}" + + consumerProguardFiles 'proguard-rules.pro' } buildTypes { diff --git a/android/proguard-rules.pro b/android/proguard-rules.pro new file mode 100644 index 000000000..cf5c28b15 --- /dev/null +++ b/android/proguard-rules.pro @@ -0,0 +1,3 @@ +# 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.** diff --git a/android/src/main/java/com/theoplayer/ReactTHEOplayerContext.kt b/android/src/main/java/com/theoplayer/ReactTHEOplayerContext.kt index 6863c6d9d..93e59c852 100644 --- a/android/src/main/java/com/theoplayer/ReactTHEOplayerContext.kt +++ b/android/src/main/java/com/theoplayer/ReactTHEOplayerContext.kt @@ -239,7 +239,8 @@ class ReactTHEOplayerContext private constructor( // Create and initialize the media session val mediaSession = MediaSessionCompat(reactContext, TAG) - // Do not let MediaButtons restart the player when the app is not visible + // Do not let MediaButtons restart the player when media session is not active. + // https://developer.android.com/media/legacy/media-buttons#restarting-inactive-mediasessions mediaSession.setMediaButtonReceiver(null) // Create a MediaSessionConnector and attach the THEOplayer instance. diff --git a/android/src/main/java/com/theoplayer/media/MediaPlaybackService.kt b/android/src/main/java/com/theoplayer/media/MediaPlaybackService.kt index 2cc98c29b..a4b0b7b46 100644 --- a/android/src/main/java/com/theoplayer/media/MediaPlaybackService.kt +++ b/android/src/main/java/com/theoplayer/media/MediaPlaybackService.kt @@ -152,6 +152,10 @@ class MediaPlaybackService : Service() { // Set mediaSession active setActive(BuildConfig.EXTENSION_MEDIASESSION) + + // Do not let MediaButtons restart the player when media session is not active. + // https://developer.android.com/media/legacy/media-buttons#restarting-inactive-mediasessions + mediaSession.setMediaButtonReceiver(null) } } diff --git a/doc/fullscreen.md b/doc/fullscreen.md index a7e96f2f2..a21121ddb 100644 --- a/doc/fullscreen.md +++ b/doc/fullscreen.md @@ -57,6 +57,31 @@ player.presentationMode = PresentationMode.fullscreen; When the player transitions back to inline mode, the view hierarchy will be restored. +### iOS home indicator + +On iOS, a visual bar at the bottom of the screen called the home indicator, allows the user to return to the device's home screen when dragging it up. This is often preceived as a disturbing visual element when viewing a stream in fullscreen mode. To hide the home indicator, The rootViewController of the application needs to be setup accordingly, via inheritance. As part of react-native-theoplayer we've prepared a basic ViewController setup (HomeIndicatorViewController) that takes care of this. To hide the home indicator you change the default rootViewcontroller from a basic UIViewController to our HomeIndicatorViewController: + +Import the react-native-theoplayer swift code: +```swift +@import react_native_theoplayer; +``` + +And, when using RCTAppDelegate in the native app: +```swift +- (UIViewController *)createRootViewController { + return [HomeIndicatorViewController new]; +} +``` + +or otherwise: +```swift +HomeIndicatorViewController *rootViewController = [HomeIndicatorViewController new]; +... +self.window.rootViewController = rootViewController; +``` + +Our iOS presentationMode changing code checks if the rootViewController is of type HomeIndicatorViewController and will, in that case, automatically take care of showing/hiding the home indicator. + ## Portals A [Portal](https://react.dev/reference/react-dom/createPortal#usage) is a well-known concept in React that diff --git a/e2e/android/app/build.gradle b/e2e/android/app/build.gradle index f78926874..a55d37ff1 100644 --- a/e2e/android/app/build.gradle +++ b/e2e/android/app/build.gradle @@ -57,7 +57,7 @@ react { /** * Set this to true to Run Proguard on Release builds to minify the Java bytecode. */ -def enableProguardInReleaseBuilds = false +def enableProguardInReleaseBuilds = true /** * The preferred build flavor of JavaScriptCore (JSC) diff --git a/e2e/patches/cavy+4.0.2.patch b/e2e/patches/cavy+4.0.2.patch new file mode 100644 index 000000000..404d1ddc5 --- /dev/null +++ b/e2e/patches/cavy+4.0.2.patch @@ -0,0 +1,68 @@ +diff --git a/node_modules/cavy/src/Reporter.js b/node_modules/cavy/src/Reporter.js +index 1cdeb38..f7e9e35 100644 +--- a/node_modules/cavy/src/Reporter.js ++++ b/node_modules/cavy/src/Reporter.js +@@ -9,7 +9,15 @@ export default class Reporter { + // Internal: Creates a websocket connection to the cavy-cli server. + onStart() { + const url = 'ws://127.0.0.1:8082/'; ++ console.debug('Creating websocket'); + this.ws = new WebSocket(url); ++ this.ws.onerror = console.error ++ this.ws.onopen = () => { ++ console.debug('Successfully opened websocket'); ++ } ++ this.ws.onclose = () => { ++ console.debug('Closing websocket'); ++ } + } + + // Internal: Send a single test result to cavy-cli over the websocket connection. +@@ -34,7 +42,6 @@ export default class Reporter { + console.log(message); + } + } +- + // Private: Determines whether data can be sent over the websocket. + websocketReady() { + // WebSocket.readyState 1 means the web socket connection is OPEN. +diff --git a/node_modules/cavy/src/TestRunner.js b/node_modules/cavy/src/TestRunner.js +index 40552bf..5b98f72 100644 +--- a/node_modules/cavy/src/TestRunner.js ++++ b/node_modules/cavy/src/TestRunner.js +@@ -148,7 +148,7 @@ export default class TestRunner { + const stop = new Date(); + const time = (stop - start) / 1000; + +- let fullErrorMessage = `${description} ❌\n ${e.message}`; ++ let fullErrorMessage = `${description} ❌\n ${JSON.stringify(e)}`; + console.warn(fullErrorMessage); + + this.results.push({ +diff --git a/node_modules/cavy/src/Tester.js b/node_modules/cavy/src/Tester.js +index c61e31a..8d222ae 100644 +--- a/node_modules/cavy/src/Tester.js ++++ b/node_modules/cavy/src/Tester.js +@@ -57,20 +57,8 @@ export default class Tester extends Component { + key: Math.random() + }; + this.testHookStore = props.store; +- // Default to sending a test report to cavy-cli if no custom reporter is +- // supplied. +- if (props.reporter instanceof Function) { +- const message = 'Deprecation warning: support for custom function' + +- 'reporters will soon be deprecated. Cavy supports custom ' + +- 'class based reporters. For more info, see the ' + +- 'documentation here: ' + +- 'https://cavy.app/docs/guides/writing-custom-reporters'; +- console.warn(message); +- this.reporter = props.reporter; +- } else { +- reporterClass = props.reporter || Reporter; +- this.reporter = new reporterClass; +- } ++ reporterClass = props.reporter || Reporter; ++ this.reporter = new reporterClass; + } + + componentDidMount() { diff --git a/e2e/src/components/TestableTHEOplayerView.tsx b/e2e/src/components/TestableTHEOplayerView.tsx index ac17ec766..1e3691c3a 100644 --- a/e2e/src/components/TestableTHEOplayerView.tsx +++ b/e2e/src/components/TestableTHEOplayerView.tsx @@ -1,23 +1,45 @@ import { useCavy } from 'cavy'; import { THEOplayer, THEOplayerView, THEOplayerViewProps } from 'react-native-theoplayer'; import React, { useCallback } from 'react'; -import { PromiseController } from '../utils/PromiseController'; -let playerController = new PromiseController(); +let testPlayer: THEOplayer | undefined = undefined; -export const getTestPlayer = async (): Promise => { - return playerController.promise_; +/** + * Wait until the player is ready. + * + * @param timeout Delay after rejecting the player. + * @param poll Delay before trying again. + */ +export const getTestPlayer = async (timeout = 5000, poll = 200): Promise => { + return new Promise((resolve, reject) => { + const start = Date.now(); + const checkPlayer = () => { + setTimeout(() => { + if (testPlayer) { + // Player is ready. + resolve(testPlayer); + } else if (Date.now() - start > timeout) { + // Too late. + reject('Player not ready'); + } else { + // Wait & try again. + checkPlayer(); + } + }, poll); + }; + checkPlayer(); + }); }; export const TestableTHEOplayerView = (props: THEOplayerViewProps) => { const generateTestHook = useCavy(); const onPlayerReady = useCallback((player: THEOplayer) => { - playerController.resolve_(player); + testPlayer = player; props.onPlayerReady?.(player); }, []); const onPlayerDestroy = useCallback(() => { - playerController = new PromiseController(); + testPlayer = undefined; }, []); return ; diff --git a/e2e/src/tests/Basic.spec.ts b/e2e/src/tests/Basic.spec.ts index 6de3f00fc..c21c2346c 100644 --- a/e2e/src/tests/Basic.spec.ts +++ b/e2e/src/tests/Basic.spec.ts @@ -6,7 +6,7 @@ import hls from '../res/hls.json'; import mp4 from '../res/mp4.json'; import { expect, preparePlayerWithSource, waitForPlayerEventType, waitForPlayerEventTypes } from '../utils/Actions'; -const SEEK_THRESHOLD = 1e-1; +const SEEK_THRESHOLD = 250; function testBasicPlayout(spec: TestScope, title: string, source: SourceDescription) { spec.describe(title, function () { diff --git a/e2e/src/tests/index.ts b/e2e/src/tests/index.ts index 441b24a0f..6e26987e2 100644 --- a/e2e/src/tests/index.ts +++ b/e2e/src/tests/index.ts @@ -4,4 +4,4 @@ import Connector from './Connector.spec'; import PresentationMode from './PresentationMode.spec'; import Version from './Version.spec'; -export default [Basic, Ads, Connector, PresentationMode, Version]; +export default [Version, Basic, Ads, Connector, PresentationMode]; diff --git a/e2e/src/utils/Actions.ts b/e2e/src/utils/Actions.ts index 29428d78a..ef2aee501 100644 --- a/e2e/src/utils/Actions.ts +++ b/e2e/src/utils/Actions.ts @@ -65,9 +65,9 @@ export const waitForPlayerEvents = async >( inOrder: boolean = true, options = defaultTestOptions, ): Promise[]> => { - return withTimeOut( + const receivedEvents: Event[] = []; + return withEventTimeOut( new Promise[]>((resolve, reject) => { - const events: Event[] = []; const onError = (err: ErrorEvent) => { console.error('[waitForPlayerEvents]', err); player.removeEventListener(PlayerEventType.ERROR, onError); @@ -81,14 +81,14 @@ export const waitForPlayerEvents = async >( return; } const expectedEvent = eventMap[0].event; - events.push(receivedEvent); + receivedEvents.push(receivedEvent); console.debug('[waitForPlayerEvents]', `Received event ${JSON.stringify(receivedEvent.type)} - waiting for ${expectedEvent.type}`); const index = eventMap.findIndex((e) => propsMatch(e.event, receivedEvent)); const isExpected = index <= 0; // Check order if (inOrder && eventMap.length && !isExpected) { - const err = `Expected event '${expectedEvent.type}'\nbut received '${receivedEvent.type}'`; + const err = `Expected event '${expectedEvent.type}' but received '${receivedEvent.type}'`; console.error('[waitForPlayerEvents]', err); reject(err); } @@ -100,7 +100,7 @@ export const waitForPlayerEvents = async >( }); if (!eventMap.length) { // Done - resolve(events); + resolve(receivedEvents); } }, })); @@ -108,13 +108,23 @@ export const waitForPlayerEvents = async >( eventMap.forEach(({ event, onEvent }) => player.addEventListener(event.type, onEvent)); }), options.timeout, + expectedEvents, + receivedEvents, ); }; -const withTimeOut = (promise: Promise, timeout: number): Promise => { +const withEventTimeOut = >( + promise: Promise, + timeout: number, + expectedEvents: Partial[], + receivedEvents: EType[], +): Promise => { return new Promise((resolve, reject) => { const handle = setTimeout(() => { - reject('Timeout waiting for event'); + reject( + `Timeout waiting for next event, expecting [${expectedEvents.map((ev) => JSON.stringify(ev)).join(',')}] ` + + `already received [${receivedEvents.map((ev) => JSON.stringify(ev)).join(',')}]`, + ); }, timeout); promise .then((result: any) => { diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index fdab8d83b..ab5bfa41c 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1,10 +1,13 @@ PODS: - boost (1.84.0) - DoubleConversion (1.1.6) + - DSFRegex (3.3.1) - FBLazyVector (0.75.4-0) - fmt (9.1.0) - glog (0.3.5) - google-cast-sdk-dynamic-xcframework (4.8.0) + - GoogleAds-IMA-iOS-SDK (3.23.0) + - GoogleAds-IMA-tvOS-SDK (4.9.1) - hermes-engine (0.75.4-0): - hermes-engine/Pre-built (= 0.75.4-0) - hermes-engine/Pre-built (0.75.4-0) @@ -1269,9 +1272,12 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - react-native-theoplayer (8.5.0): + - react-native-theoplayer (8.7.0): - React-Core - - THEOplayerSDK-core (~> 8.0) + - THEOplayer-Connector-SideloadedSubtitle (~> 8.3) + - THEOplayer-Integration-GoogleCast (~> 8.3) + - THEOplayer-Integration-GoogleIMA (~> 8.3) + - THEOplayerSDK-core (~> 8.3) - React-nativeconfig (0.75.4-0) - React-NativeModulesApple (0.75.4-0): - glog @@ -1535,7 +1541,29 @@ PODS: - RNSVG (13.14.1): - React-Core - SocketRocket (0.7.0) + - Swifter (1.5.0) + - SwiftSubtitles (0.9.1): + - DSFRegex (~> 3.3.1) + - TinyCSV (~> 0.6.1) + - THEOplayer-Connector-SideloadedSubtitle (8.3.0): + - Swifter (= 1.5.0) + - SwiftSubtitles (= 0.9.1) + - THEOplayerSDK-core (~> 8) + - THEOplayer-Integration-GoogleCast (8.3.0): + - THEOplayer-Integration-GoogleCast/Base (= 8.3.0) + - THEOplayer-Integration-GoogleCast/Dependencies (= 8.3.0) + - THEOplayer-Integration-GoogleCast/Base (8.3.0) + - THEOplayer-Integration-GoogleCast/Dependencies (8.3.0): + - google-cast-sdk-dynamic-xcframework (~> 4.8) + - THEOplayer-Integration-GoogleIMA (8.3.0): + - THEOplayer-Integration-GoogleIMA/Base (= 8.3.0) + - THEOplayer-Integration-GoogleIMA/Dependencies (= 8.3.0) + - THEOplayer-Integration-GoogleIMA/Base (8.3.0) + - THEOplayer-Integration-GoogleIMA/Dependencies (8.3.0): + - GoogleAds-IMA-iOS-SDK (~> 3.18) + - GoogleAds-IMA-tvOS-SDK (~> 4.8) - THEOplayerSDK-core (8.3.0) + - TinyCSV (0.6.1) - Yoga (0.0.0) DEPENDENCIES: @@ -1610,10 +1638,19 @@ DEPENDENCIES: SPEC REPOS: trunk: + - DSFRegex - google-cast-sdk-dynamic-xcframework + - GoogleAds-IMA-iOS-SDK + - GoogleAds-IMA-tvOS-SDK - PromisesObjC - SocketRocket + - Swifter + - SwiftSubtitles + - THEOplayer-Connector-SideloadedSubtitle + - THEOplayer-Integration-GoogleCast + - THEOplayer-Integration-GoogleIMA - THEOplayerSDK-core + - TinyCSV EXTERNAL SOURCES: boost: @@ -1759,10 +1796,13 @@ CHECKOUT OPTIONS: SPEC CHECKSUMS: boost: d70f09e8edc61001a5cd2131f47cca76f9b3f031 DoubleConversion: 00143ab27d470b28035933623e1a3ea37e68889c + DSFRegex: 8493187c71ac199695245eb9ec98bad4f87a2f0b FBLazyVector: e06894178a2469b6da988d1d4de56aca5a3f90d1 fmt: 1568fa7b2f242362c45c42d4a15e9dd4b2e621b3 glog: 4d211b5b727f9d4542418484bf9945f28b8cb4a5 google-cast-sdk-dynamic-xcframework: d1323732742c979b2d7e5b061cbe665915981f3d + GoogleAds-IMA-iOS-SDK: ee2a68ed7a1a17c7bb81bdb1b81590b35a3fc8f3 + GoogleAds-IMA-tvOS-SDK: 85e799c35051454693492480ef7e4ae2e701a05f hermes-engine: ac68d6c3169772a7a7f9eeb25dbb5ff87930034f PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 RCT-Folly: 4728c42e04357ad445c7048e7c542b59f3ee6b4f @@ -1796,7 +1836,7 @@ SPEC CHECKSUMS: React-microtasksnativemodule: a3489ca37b515f6f685ec1a86c8df364343ac578 react-native-google-cast: d7bdfd1a0eeba84afde03b9722351ec29543e74c react-native-slider: caf709802c97955d1dc369fc2ca6250f18bd58fb - react-native-theoplayer: 17d8a46df5e3e6e75f9afb96f5d409e5e0384d3d + react-native-theoplayer: 9ace968b751d79999f9c6ae6c21e80b6d143975c React-nativeconfig: ea22f0ab525feb865d2e0ed5d7aad156c36abe6b React-NativeModulesApple: 5efee2e69aaa7ff47f40a2918f2b48534a2e431b React-perflogger: f31660a8693c3444e1832c237ba25a13f613436e @@ -1825,8 +1865,14 @@ SPEC CHECKSUMS: ReactCommon: a1cd388360d4e9545ab8e571ff293606c9ea6687 RNSVG: af3907ac5d4fa26a862b75a16d8f15bc74f2ceda SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d + Swifter: e71dd674404923d7f03ebb03f3f222d1c570bc8e + SwiftSubtitles: c659af19d710a2946779015464c0577d07fe4666 + THEOplayer-Connector-SideloadedSubtitle: cc36449bedaaa44a21cff6156bdddc71f767d1e6 + THEOplayer-Integration-GoogleCast: e4fb3f3ae2022b079906f42ec708ce96579cb629 + THEOplayer-Integration-GoogleIMA: 7310ca6eb20628f2b774905bbead37e1d9105d24 THEOplayerSDK-core: 03e55ca6dfe3f16d52fdc9e4bdc6bff150d63531 - Yoga: 07ebe50bd234e51e5e3e07befa14a3078a0fcbbd + TinyCSV: fd6228edbcf1c07466ac34b76dac5e052143eaba + Yoga: 1eb8c4882b3018c344a2ff61c5f2e5c6b1711d82 PODFILE CHECKSUM: cde44975d89d0413eb0e568fb525618efb0ece50 diff --git a/example/ios/ReactNativeTHEOplayer.xcodeproj/project.pbxproj b/example/ios/ReactNativeTHEOplayer.xcodeproj/project.pbxproj index 713bf9e28..a6db2fd8d 100644 --- a/example/ios/ReactNativeTHEOplayer.xcodeproj/project.pbxproj +++ b/example/ios/ReactNativeTHEOplayer.xcodeproj/project.pbxproj @@ -502,13 +502,17 @@ ); inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-ReactNativeTHEOplayer-tvOS/Pods-ReactNativeTHEOplayer-tvOS-frameworks.sh", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/THEOplayer-Integration-GoogleIMA/Base/THEOplayerGoogleIMAIntegration.framework/THEOplayerGoogleIMAIntegration", "${PODS_XCFRAMEWORKS_BUILD_DIR}/THEOplayerSDK-core/THEOplayerSDK.framework/THEOplayerSDK", "${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/GoogleAds-IMA-tvOS-SDK/GoogleInteractiveMediaAds.framework/GoogleInteractiveMediaAds", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/THEOplayerGoogleIMAIntegration.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/THEOplayerSDK.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleInteractiveMediaAds.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -544,12 +548,18 @@ ); inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-ReactNativeTHEOplayer-ReactNativeTHEOplayerTests/Pods-ReactNativeTHEOplayer-ReactNativeTHEOplayerTests-frameworks.sh", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/GoogleAds-IMA-iOS-SDK/GoogleInteractiveMediaAds.framework/GoogleInteractiveMediaAds", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/THEOplayer-Integration-GoogleCast/Base/THEOplayerGoogleCastIntegration.framework/THEOplayerGoogleCastIntegration", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/THEOplayer-Integration-GoogleIMA/Base/THEOplayerGoogleIMAIntegration.framework/THEOplayerGoogleIMAIntegration", "${PODS_XCFRAMEWORKS_BUILD_DIR}/THEOplayerSDK-core/THEOplayerSDK.framework/THEOplayerSDK", "${PODS_XCFRAMEWORKS_BUILD_DIR}/google-cast-sdk-dynamic-xcframework/GoogleCast.framework/GoogleCast", "${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleInteractiveMediaAds.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/THEOplayerGoogleCastIntegration.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/THEOplayerGoogleIMAIntegration.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/THEOplayerSDK.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleCast.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework", @@ -566,13 +576,17 @@ ); inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-ReactNativeTHEOplayer-tvOS-ReactNativeTHEOplayer-tvOSTests/Pods-ReactNativeTHEOplayer-tvOS-ReactNativeTHEOplayer-tvOSTests-frameworks.sh", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/THEOplayer-Integration-GoogleIMA/Base/THEOplayerGoogleIMAIntegration.framework/THEOplayerGoogleIMAIntegration", "${PODS_XCFRAMEWORKS_BUILD_DIR}/THEOplayerSDK-core/THEOplayerSDK.framework/THEOplayerSDK", "${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/GoogleAds-IMA-tvOS-SDK/GoogleInteractiveMediaAds.framework/GoogleInteractiveMediaAds", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/THEOplayerGoogleIMAIntegration.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/THEOplayerSDK.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleInteractiveMediaAds.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -720,12 +734,18 @@ ); inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-ReactNativeTHEOplayer/Pods-ReactNativeTHEOplayer-frameworks.sh", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/GoogleAds-IMA-iOS-SDK/GoogleInteractiveMediaAds.framework/GoogleInteractiveMediaAds", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/THEOplayer-Integration-GoogleCast/Base/THEOplayerGoogleCastIntegration.framework/THEOplayerGoogleCastIntegration", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/THEOplayer-Integration-GoogleIMA/Base/THEOplayerGoogleIMAIntegration.framework/THEOplayerGoogleIMAIntegration", "${PODS_XCFRAMEWORKS_BUILD_DIR}/THEOplayerSDK-core/THEOplayerSDK.framework/THEOplayerSDK", "${PODS_XCFRAMEWORKS_BUILD_DIR}/google-cast-sdk-dynamic-xcframework/GoogleCast.framework/GoogleCast", "${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleInteractiveMediaAds.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/THEOplayerGoogleCastIntegration.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/THEOplayerGoogleIMAIntegration.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/THEOplayerSDK.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleCast.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework", @@ -838,7 +858,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 8YAB8ZY55Y; + DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; GCC_PREPROCESSOR_DEFINITIONS = ( "$(inherited)", diff --git a/example/ios/ReactNativeTHEOplayer/AppDelegate.h b/example/ios/ReactNativeTHEOplayer/AppDelegate.h index ef1de86a2..5d2808256 100644 --- a/example/ios/ReactNativeTHEOplayer/AppDelegate.h +++ b/example/ios/ReactNativeTHEOplayer/AppDelegate.h @@ -1,8 +1,6 @@ -#import +#import #import -@interface AppDelegate : UIResponder - -@property (nonatomic, strong) UIWindow *window; +@interface AppDelegate : RCTAppDelegate @end diff --git a/example/ios/ReactNativeTHEOplayer/AppDelegate.m b/example/ios/ReactNativeTHEOplayer/AppDelegate.m index 83cb1a150..66bac0eb1 100644 --- a/example/ios/ReactNativeTHEOplayer/AppDelegate.m +++ b/example/ios/ReactNativeTHEOplayer/AppDelegate.m @@ -1,8 +1,7 @@ #import "AppDelegate.h" -#import #import -#import +@import react_native_theoplayer; #if !TARGET_OS_TV #import @@ -12,20 +11,10 @@ @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions]; - - RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge - moduleName:@"ReactNativeTHEOplayer" - initialProperties:nil]; - - rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1]; - - self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; - UIViewController *rootViewController = [UIViewController new]; - - rootViewController.view = rootView; - self.window.rootViewController = rootViewController; - [self.window makeKeyAndVisible]; + self.moduleName = @"ReactNativeTHEOplayer"; + // You can add your custom initial props in the dictionary below. + // They will be passed down to the ViewController used by React Native. + self.initialProps = @{}; #if !TARGET_OS_TV NSString *receiverAppID = @"CC1AD845"; // default receiver @@ -36,7 +25,11 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( [GCKCastContext setSharedInstanceWithOptions:options]; #endif - return YES; + return [super application:application didFinishLaunchingWithOptions:launchOptions]; +} + +- (UIViewController *)createRootViewController { + return [HomeIndicatorViewController new]; } - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge diff --git a/ios/THEOplayerRCTView.swift b/ios/THEOplayerRCTView.swift index 7703267fe..4822d1eed 100644 --- a/ios/THEOplayerRCTView.swift +++ b/ios/THEOplayerRCTView.swift @@ -9,6 +9,7 @@ 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 @@ -76,6 +77,14 @@ public class THEOplayerRCTView: UIView { 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 + } } } diff --git a/ios/presentationMode/THEOplayerRCTPresentationModeManager.swift b/ios/presentationMode/THEOplayerRCTPresentationModeManager.swift index 7b2cd94d9..9544c1ca7 100644 --- a/ios/presentationMode/THEOplayerRCTPresentationModeManager.swift +++ b/ios/presentationMode/THEOplayerRCTPresentationModeManager.swift @@ -14,7 +14,6 @@ public class THEOplayerRCTPresentationModeManager { private var containerView: UIView? // view containing the playerView and it's siblings (e.g. UI) private var inlineParentView: UIView? // target view for inline representation - private var movingChildVCs: [UIViewController] = [] // list of playerView's child VCs that need to be reparented while moving the playerView // MARK: Events var onNativePresentationModeChange: RCTDirectEventBlock? @@ -39,35 +38,21 @@ public class THEOplayerRCTPresentationModeManager { // MARK: - logic - private func storeMovingVCs(for view: UIView) { - if let viewController = view.findViewController() { - viewController.children.forEach { childVC in - self.movingChildVCs.append(childVC) - } - } - } - - private func clearMovingVCs() { - self.movingChildVCs = [] - } - - private func moveView(_ movingView: UIView, to targetView: UIView, with movingViewControllers: [UIViewController]) { - // detach the moving viewControllers from their parent - movingViewControllers.forEach { movedVC in - movedVC.removeFromParent() - } + private func moveView(_ movingView: UIView, to targetView: UIView) { + guard let theoPlayerViewController = (self.view as? THEOplayerRCTView)?.theoPlayerViewController else { return } + + // detach the viewController from its parent + theoPlayerViewController.removeFromParent() // move the actual view movingView.removeFromSuperview() targetView.addSubview(movingView) targetView.bringSubviewToFront(movingView) - // attach the moving viewControllers to their new parent + // attach the viewController to its new parent if let targetViewController = targetView.findViewController() { - movingViewControllers.forEach { movedVC in - targetViewController.addChild(movedVC) - movedVC.didMove(toParent: targetViewController) - } + targetViewController.addChild(theoPlayerViewController) + theoPlayerViewController.didMove(toParent: targetViewController) } } @@ -75,19 +60,32 @@ public class THEOplayerRCTPresentationModeManager { self.containerView = self.view?.findParentViewOfType(RCTView.self) self.inlineParentView = self.containerView?.findParentViewOfType(RCTView.self) + // move the player if let containerView = self.containerView, let fullscreenParentView = self.view?.findParentViewOfType(RCTRootContentView.self) { - self.storeMovingVCs(for: containerView) - self.moveView(containerView, to: fullscreenParentView, with: self.movingChildVCs) + self.moveView(containerView, to: fullscreenParentView) + + // start hiding home indicator + if let customRootViewController = fullscreenParentView.findViewController() as? HomeIndicatorViewController { + customRootViewController.prefersAutoHidden = true + customRootViewController.setNeedsUpdateOfHomeIndicatorAutoHidden() + } } self.rnInlineMode = .fullscreen } private func exitFullscreen() { + // stop hiding home indicator + if let fullscreenParentView = self.view?.findParentViewOfType(RCTRootContentView.self), + let customRootViewController = fullscreenParentView.findViewController() as? HomeIndicatorViewController { + customRootViewController.prefersAutoHidden = false + customRootViewController.setNeedsUpdateOfHomeIndicatorAutoHidden() + } + + // move the player if let containerView = self.containerView, let inlineParentView = self.inlineParentView { - self.moveView(containerView, to: inlineParentView, with: self.movingChildVCs) - self.clearMovingVCs() + self.moveView(containerView, to: inlineParentView) } self.rnInlineMode = .inline } diff --git a/ios/viewController/HomeIndicatorViewController.swift b/ios/viewController/HomeIndicatorViewController.swift new file mode 100644 index 000000000..5caeef7ea --- /dev/null +++ b/ios/viewController/HomeIndicatorViewController.swift @@ -0,0 +1,14 @@ +// HomeIndicatorViewController.swift + +import UIKit + +@objc +public class HomeIndicatorViewController: UIViewController { + public var prefersAutoHidden = false + + public override var prefersHomeIndicatorAutoHidden: Bool { + get { + return self.prefersAutoHidden + } + } +} diff --git a/react-native-theoplayer.podspec b/react-native-theoplayer.podspec index 92975b31e..66af13343 100644 --- a/react-native-theoplayer.podspec +++ b/react-native-theoplayer.podspec @@ -25,7 +25,7 @@ Pod::Spec.new do |s| s.platforms = { :ios => "13.4", :tvos => "13.4" } s.source = { :git => "https://www.theoplayer.com/.git", :tag => "#{s.version}" } - s.source_files = 'ios/*.{h,m,swift}', 'ios/ads/*.swift', 'ios/casting/*.swift', 'ios/contentprotection/*.swift', 'ios/pip/*.swift', 'ios/backgroundAudio/*.swift', 'ios/cache/*.swift', 'ios/sideloadedMetadata/*.swift', 'ios/eventBroadcasting/*.swift' , 'ios/ui/*.swift', 'ios/presentationMode/*.swift' + s.source_files = 'ios/*.{h,m,swift}', 'ios/ads/*.swift', 'ios/casting/*.swift', 'ios/contentprotection/*.swift', 'ios/pip/*.swift', 'ios/backgroundAudio/*.swift', 'ios/cache/*.swift', 'ios/sideloadedMetadata/*.swift', 'ios/eventBroadcasting/*.swift' , 'ios/ui/*.swift', 'ios/presentationMode/*.swift', 'ios/viewController/*.swift' s.resources = ['ios/*.css'] # ReactNative Dependency