Skip to content

Commit

Permalink
Merge pull request #440 from THEOplayer/release/v8.8.0
Browse files Browse the repository at this point in the history
Release/v8.8.0
  • Loading branch information
tvanlaerhoven authored Nov 19, 2024
2 parents c7f021e + 6343f74 commit dbafadb
Show file tree
Hide file tree
Showing 21 changed files with 302 additions and 77 deletions.
12 changes: 6 additions & 6 deletions .github/workflows/pr_ios.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,16 @@ 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

- 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
Expand Down Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
3 changes: 3 additions & 0 deletions android/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -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.**
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down
25 changes: 25 additions & 0 deletions doc/fullscreen.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion e2e/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
68 changes: 68 additions & 0 deletions e2e/patches/cavy+4.0.2.patch
Original file line number Diff line number Diff line change
@@ -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() {
34 changes: 28 additions & 6 deletions e2e/src/components/TestableTHEOplayerView.tsx
Original file line number Diff line number Diff line change
@@ -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<THEOplayer>();
let testPlayer: THEOplayer | undefined = undefined;

export const getTestPlayer = async (): Promise<THEOplayer> => {
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<THEOplayer> => {
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<THEOplayer>();
testPlayer = undefined;
}, []);

return <THEOplayerView ref={generateTestHook('Scene.THEOplayerView')} {...props} onPlayerReady={onPlayerReady} onPlayerDestroy={onPlayerDestroy} />;
Expand Down
2 changes: 1 addition & 1 deletion e2e/src/tests/Basic.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down
2 changes: 1 addition & 1 deletion e2e/src/tests/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
24 changes: 17 additions & 7 deletions e2e/src/utils/Actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,9 @@ export const waitForPlayerEvents = async <EType extends Event<PlayerEventType>>(
inOrder: boolean = true,
options = defaultTestOptions,
): Promise<Event<PlayerEventType>[]> => {
return withTimeOut(
const receivedEvents: Event<PlayerEventType>[] = [];
return withEventTimeOut(
new Promise<Event<PlayerEventType>[]>((resolve, reject) => {
const events: Event<PlayerEventType>[] = [];
const onError = (err: ErrorEvent) => {
console.error('[waitForPlayerEvents]', err);
player.removeEventListener(PlayerEventType.ERROR, onError);
Expand All @@ -81,14 +81,14 @@ export const waitForPlayerEvents = async <EType extends Event<PlayerEventType>>(
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);
}
Expand All @@ -100,21 +100,31 @@ export const waitForPlayerEvents = async <EType extends Event<PlayerEventType>>(
});
if (!eventMap.length) {
// Done
resolve(events);
resolve(receivedEvents);
}
},
}));
player.addEventListener(PlayerEventType.ERROR, onError);
eventMap.forEach(({ event, onEvent }) => player.addEventListener(event.type, onEvent));
}),
options.timeout,
expectedEvents,
receivedEvents,
);
};

const withTimeOut = (promise: Promise<any>, timeout: number): Promise<any> => {
const withEventTimeOut = <EType extends Event<PlayerEventType>>(
promise: Promise<any>,
timeout: number,
expectedEvents: Partial<EType>[],
receivedEvents: EType[],
): Promise<any> => {
return new Promise<void>((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) => {
Expand Down
Loading

0 comments on commit dbafadb

Please sign in to comment.