diff --git a/.gitignore b/.gitignore index 5fab2bb9c..b16632daa 100644 --- a/.gitignore +++ b/.gitignore @@ -77,8 +77,14 @@ ios/custom/Frameworks/tvos/*.xcframework # Jekyll / GitHub Pages _site/ + Gemfile.lock vendor .bundle src/manifest.json + +# E2E app e2e/cavy_results.md + +# Example app +example/dist diff --git a/CHANGELOG.md b/CHANGELOG.md index c44f25862..95ff3a5fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,13 +5,25 @@ 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.11.1] - 24-12-18 + +### Fixed + +- Fixed the picture-in-picture presentationMode for THEOlive sources on Web. + +### Changed + +- Deprecated the use of the `enableTHEOlive` flag in `PlayerConfiguration` as THEOlive support is always enabled. + ## [8.11.0] - 24-12-13 +### Added + - Added support for THEOlive on tvOS. ## [8.10.0] - 24-12-06 -## Added +### Added - Added support for THEOlive on iOS. diff --git a/e2e/src/tests/Ads.spec.ts b/e2e/src/tests/Ads.spec.ts index 497de47a3..be4eb680e 100644 --- a/e2e/src/tests/Ads.spec.ts +++ b/e2e/src/tests/Ads.spec.ts @@ -1,15 +1,9 @@ import { TestScope } from 'cavy'; -import { AdDescription, AdEventType, PlayerEventType, SourceDescription, AdEvent } from 'react-native-theoplayer'; -import hls from '../res/hls.json'; -import ads from '../res/ads.json'; +import { AdEventType, PlayerEventType, AdEvent } from 'react-native-theoplayer'; import { getTestPlayer } from '../components/TestableTHEOplayerView'; import { waitForPlayerEvents, waitForPlayerEventTypes } from '../utils/Actions'; import { TestSourceDescription, TestSources } from '../utils/SourceUtils'; -function extendSourceWithAds(source: SourceDescription, ad: AdDescription): SourceDescription { - return { ...source, ads: [ad] }; -} - export default function (spec: TestScope) { TestSources() .withAds() @@ -26,7 +20,7 @@ export default function (spec: TestScope) { // Start autoplay player.autoplay = true; - player.source = extendSourceWithAds(hls[0], ads[0] as AdDescription); + player.source = testSource.source; // Expect events. await playEventsPromise; diff --git a/e2e/src/tests/Connector.spec.ts b/e2e/src/tests/Connector.spec.ts index bf4a4c077..1695fcaa4 100644 --- a/e2e/src/tests/Connector.spec.ts +++ b/e2e/src/tests/Connector.spec.ts @@ -50,7 +50,7 @@ export default function (spec: TestScope) { {}, { customerKey: 'testCustomerKey', - gatewayUrl: 'testGgatewayUrl', + gatewayUrl: 'testGatewayUrl', }, ); }, diff --git a/e2e/src/tests/PresentationMode.spec.ts b/e2e/src/tests/PresentationMode.spec.ts index 9fee9d28a..818cb53ea 100644 --- a/e2e/src/tests/PresentationMode.spec.ts +++ b/e2e/src/tests/PresentationMode.spec.ts @@ -33,7 +33,7 @@ export default function (spec: TestScope) { const inlinePromise = waitForPlayerEvent(player, { type: PlayerEventType.PRESENTATIONMODE_CHANGE, presentationMode: PresentationMode.inline, - previousPresentationMode: PresentationMode.inline, + previousPresentationMode: PresentationMode.fullscreen, } as PresentationModeChangeEvent); player.presentationMode = PresentationMode.inline; diff --git a/e2e/src/utils/Actions.ts b/e2e/src/utils/Actions.ts index ef2aee501..be0548c0c 100644 --- a/e2e/src/utils/Actions.ts +++ b/e2e/src/utils/Actions.ts @@ -59,6 +59,7 @@ export const waitForPlayerEvent = async >( return waitForPlayerEvents(player, [expectedEvent], false, options); }; +let eventListIndex = 0; // increments for every playerEvent list that is evaluated. export const waitForPlayerEvents = async >( player: THEOplayer, expectedEvents: Partial[], @@ -73,39 +74,58 @@ export const waitForPlayerEvents = async >( player.removeEventListener(PlayerEventType.ERROR, onError); reject(err); }; - let eventMap = expectedEvents.map((_expected: Partial) => ({ - event: _expected as Event, - onEvent(receivedEvent: Event) { - if (!eventMap.length) { - // No more events expected - return; - } - const expectedEvent = eventMap[0].event; + + const TAG: string = `[waitForPlayerEvents] eventList ${eventListIndex}:`; + eventListIndex += 1; + + let unReceivedEvents = [...expectedEvents]; + const uniqueEventTypes = [...new Set(unReceivedEvents.map((event) => event.type))]; + uniqueEventTypes.forEach((eventType) => { + const onEvent = (receivedEvent: Event) => { 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}' but received '${receivedEvent.type}'`; - console.error('[waitForPlayerEvents]', err); - reject(err); + if (inOrder && unReceivedEvents.length) { + const expectedEvent = unReceivedEvents[0]; + console.debug(TAG, `Handling received event ${JSON.stringify(receivedEvent)}`); + console.debug(TAG, `Was waiting for ${JSON.stringify(expectedEvent)}`); + + // Received events must either not be in the expected, or be the first + const index = unReceivedEvents.findIndex((e) => propsMatch(e, receivedEvent)); + if (index > 0) { + const err = `Expected '${expectedEvent.type}' event but received '${receivedEvent.type} event'`; + console.error(TAG, err); + reject(err); + } else { + console.debug(TAG, `Received ${receivedEvent.type} event is allowed.`); + } } - eventMap = eventMap.filter((entry) => { - if (entry.event.type === expectedEvent.type) { - player.removeEventListener(expectedEvent.type, entry.onEvent); + + unReceivedEvents = unReceivedEvents.filter((event) => { + // When found, remove the listener + if (propsMatch(event, receivedEvent)) { + console.debug(TAG, ` -> removing: ${JSON.stringify(event)}`); + return false; } - return entry.event.type !== expectedEvent.type; + // Only keep the unreceived events + console.debug(TAG, ` -> keeping: ${JSON.stringify(event)}`); + return true; }); - if (!eventMap.length) { - // Done + + // remove listener if no other unreceived events require it. + if (!unReceivedEvents.find((event) => event.type === receivedEvent.type)) { + console.debug(TAG, `Removing listener for ${receivedEvent.type} from player`); + player.removeEventListener(receivedEvent.type, onEvent); + } + + if (!unReceivedEvents.length) { + // Finished resolve(receivedEvents); } - }, - })); + }; + + player.addEventListener(eventType as PlayerEventType, onEvent); + console.debug(TAG, `Added listener for ${eventType} to the player`); + }); player.addEventListener(PlayerEventType.ERROR, onError); - eventMap.forEach(({ event, onEvent }) => player.addEventListener(event.type, onEvent)); }), options.timeout, expectedEvents, diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index 4cebc6b29..8aac02b3b 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -3,20 +3,28 @@ + + + + + android:supportsRtl="true" + android:theme="@style/AppTheme"> ) { const { config, children } = props; @@ -10,15 +9,6 @@ export function THEOplayerView(props: React.PropsWithChildren(null); const container = useRef(null); - const preparePlayer = async (adapter: THEOplayerWebAdapter) => { - if (config?.enableTHEOlive == true && browserCanPlayHLSAndHasNoMSE()) { - await registerServiceWorker(props.config?.libraryLocation); - } - - // Notify the player is ready - props.onPlayerReady?.(adapter); - }; - useEffect(() => { // Create player inside container. if (container.current) { @@ -59,8 +49,8 @@ export function THEOplayerView(props: React.PropsWithChildren { - if ('serviceWorker' in navigator) { - try { - const serviceWorkerName = 'theoplayer.sw.js'; - const serviceWorkerPath = libraryLocation - ? (libraryLocation.endsWith('/') ? libraryLocation : `${libraryLocation}/`) + serviceWorkerName - : serviceWorkerName; - const serviceWorkerScope = libraryLocation ? (libraryLocation.endsWith('/') ? libraryLocation : `${libraryLocation}/`) : '/'; - - // unregister beforehand to solve an issue when doing a hard reload of the page causing the service worker to not intercept the manifests. - await maybeUnregisterServiceWorker(serviceWorkerPath); - await navigator.serviceWorker.register(serviceWorkerPath, { - scope: serviceWorkerScope, - }); - //console.log('Successfully registered server worker'); - } catch (error) { - console.error(`Service worker registration failed: ${error}`); - } - } -} - -export async function maybeUnregisterServiceWorker(serviceWorkerPath?: string): Promise { - if ('serviceWorker' in navigator) { - try { - const registration = await navigator.serviceWorker.getRegistration(serviceWorkerPath); - await registration?.unregister(); - } catch (error) { - console.error(`Service worker unregistration failed: ${error}`); - } - } -}