diff --git a/android/build.gradle b/android/build.gradle
index 73230734..a0b81e55 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -92,4 +92,5 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "com.amazonaws:ivs-player:$ivs_version"
implementation("com.squareup.okhttp3:okhttp:5.2.1")
+ implementation "androidx.media:media:1.6.0"
}
diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml
index f8eb3953..ac827a74 100644
--- a/android/src/main/AndroidManifest.xml
+++ b/android/src/main/AndroidManifest.xml
@@ -1,5 +1,6 @@
+ package="com.amazonaws.ivs.reactnative.player">
+
+
+
diff --git a/android/src/main/java/com/amazonaws/ivs/reactnative/player/AmazonIvsView.kt b/android/src/main/java/com/amazonaws/ivs/reactnative/player/AmazonIvsView.kt
index 5138ef67..2483c46a 100644
--- a/android/src/main/java/com/amazonaws/ivs/reactnative/player/AmazonIvsView.kt
+++ b/android/src/main/java/com/amazonaws/ivs/reactnative/player/AmazonIvsView.kt
@@ -1,12 +1,20 @@
package com.amazonaws.ivs.reactnative.player
+import android.Manifest
import android.app.Activity
import android.app.PictureInPictureParams
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.util.Log
+import android.view.View
import android.widget.FrameLayout
+import androidx.core.app.ActivityCompat
+import androidx.core.content.ContextCompat
import com.amazonaws.ivs.player.Cue
import com.amazonaws.ivs.player.MediaPlayer
import com.amazonaws.ivs.player.Player
@@ -36,6 +44,7 @@ class AmazonIvsView(private val context: ThemedReactContext) : FrameLayout(conte
private var player: Player? = null
private var streamUri: Uri? = null
private val playerListener: Player.Listener?
+ private var controlReceiver: BroadcastReceiver? = null
var playerObserver: Timer? = null
private var lastLiveLatency: Long? = null
@@ -51,6 +60,10 @@ class AmazonIvsView(private val context: ThemedReactContext) : FrameLayout(conte
private var lastPosition: Long = 0
private var progressInterval: Long = 1000
+ private var wasPlayingBeforeBackground: Boolean = false
+ private var playInBackground: Boolean = false
+ private var notificationTitle: String = ""
+ private var notificationText: String = ""
private val eventDispatcher: EventDispatcher
@@ -86,6 +99,17 @@ class AmazonIvsView(private val context: ThemedReactContext) : FrameLayout(conte
context.addLifecycleEventListener(this)
+ playerView?.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
+ override fun onViewAttachedToWindow(v: View) {
+ }
+
+ override fun onViewDetachedFromWindow(v: View) {
+ if (!playInBackground) {
+ playerView?.player?.pause()
+ }
+ }
+ })
+
playerListener = object : Player.Listener() {
override fun onStateChanged(state: Player.State) {
onPlayerStateChange(state)
@@ -124,6 +148,26 @@ class AmazonIvsView(private val context: ThemedReactContext) : FrameLayout(conte
}
}
+ controlReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context?, intent: Intent?) {
+ val action = intent?.getStringExtra("action")
+ if (action == "pause") {
+ player?.pause()
+ if (playInBackground) startBackgroundService(false)
+ } else if (action == "play") {
+ player?.play()
+ if (playInBackground) startBackgroundService(true)
+ }
+ }
+ }
+
+ val filter = IntentFilter("IVS_PLAYER_CONTROL")
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ context.registerReceiver(controlReceiver, filter, Context.RECEIVER_EXPORTED)
+ } else {
+ context.registerReceiver(controlReceiver, filter, Context.RECEIVER_NOT_EXPORTED)
+ }
+
player?.addListener(playerListener);
addView(playerView)
@@ -140,11 +184,28 @@ class AmazonIvsView(private val context: ThemedReactContext) : FrameLayout(conte
}
}
+ private fun startBackgroundService(isPlaying: Boolean) {
+ val context = context.applicationContext
+ val serviceIntent = Intent(context, IVSBackgroundService::class.java)
+ serviceIntent.putExtra(IVSBackgroundService.EXTRA_IS_PLAYING, isPlaying)
+ serviceIntent.putExtra(IVSBackgroundService.NOTIFICATION_TITLE, this.notificationTitle)
+ serviceIntent.putExtra(IVSBackgroundService.NOTIFICATION_TEXT, this.notificationText)
+
+ ContextCompat.startForegroundService(context, serviceIntent)
+ }
+
+ private fun stopBackgroundService() {
+ val context = context.applicationContext
+ val serviceIntent = Intent(context, IVSBackgroundService::class.java)
+ context.stopService(serviceIntent)
+ }
+
fun setStreamUrl(streamUrl: String) {
player?.let { player ->
val reactContext = context as ReactContext
val uri = Uri.parse(streamUrl);
this.streamUri = uri;
+ playInBackground = true
finishedLoading = false
player.load(uri)
@@ -163,6 +224,29 @@ class AmazonIvsView(private val context: ThemedReactContext) : FrameLayout(conte
player?.isMuted = muted
}
+ fun setPlayInBackground(playInBackground: Boolean) {
+ if (playInBackground) {
+ checkAndRequestNotificationPermission()
+ }
+ this.playInBackground = playInBackground
+ }
+
+ fun setNotificationTitle(notificationTitle: String?) {
+ this.notificationTitle = notificationTitle ?: ""
+
+ if (isInBackground && playInBackground) {
+ startBackgroundService(player?.state == Player.State.PLAYING)
+ }
+ }
+
+ fun setNotificationText(notificationText: String?) {
+ this.notificationText = notificationText ?: ""
+
+ if (isInBackground && playInBackground) {
+ startBackgroundService(player?.state == Player.State.PLAYING)
+ }
+ }
+
fun setLooping(shouldLoop: Boolean) {
player?.setLooping(shouldLoop)
}
@@ -434,6 +518,7 @@ class AmazonIvsView(private val context: ThemedReactContext) : FrameLayout(conte
fun onPlayerStateChange(state: Player.State) {
val reactContext = context as ReactContext
+ this.keepScreenOn = (state == Player.State.PLAYING || state == Player.State.BUFFERING)
when (state) {
Player.State.PLAYING -> {
@@ -686,7 +771,7 @@ class AmazonIvsView(private val context: ThemedReactContext) : FrameLayout(conte
)
}
- fun setProgressInterval(progressInterval: Int) {
+ fun setProgressInterval(progressInterval: Double) {
playerObserver?.cancel()
playerObserver?.purge()
playerObserver = Timer("observerInterval", false)
@@ -701,12 +786,30 @@ class AmazonIvsView(private val context: ThemedReactContext) : FrameLayout(conte
override fun onHostResume() {
isInBackground = false
+ stopBackgroundService()
+
+ if (wasPlayingBeforeBackground) {
+ player?.play()
+ wasPlayingBeforeBackground = false
+ }
}
override fun onHostPause() {
if (pipEnabled) {
isInBackground = true
togglePip()
+ } else {
+ if (playInBackground && player?.state == Player.State.PLAYING) {
+ startBackgroundService(true)
+ return
+ }
+
+ if (player?.state == Player.State.PLAYING || player?.state == Player.State.BUFFERING) {
+ wasPlayingBeforeBackground = true
+ player?.pause()
+ } else {
+ wasPlayingBeforeBackground = false
+ }
}
}
@@ -715,6 +818,7 @@ class AmazonIvsView(private val context: ThemedReactContext) : FrameLayout(conte
}
fun cleanup() {
+ stopBackgroundService()
// Cleanup any remaining sources
for (source in preloadSourceMap.values) {
source.release()
@@ -727,6 +831,11 @@ class AmazonIvsView(private val context: ThemedReactContext) : FrameLayout(conte
playerObserver?.cancel()
playerObserver = null
+
+ if (controlReceiver != null) {
+ context.unregisterReceiver(controlReceiver)
+ controlReceiver = null
+ }
}
fun TextCue.TextAlignment.toStringValue(): String {
@@ -737,4 +846,25 @@ class AmazonIvsView(private val context: ThemedReactContext) : FrameLayout(conte
}
}
+ private fun checkAndRequestNotificationPermission() {
+ if (Build.VERSION.SDK_INT >= 33) {
+ val reactContext = context as ReactContext
+ val activity = reactContext.currentActivity
+
+ if (ContextCompat.checkSelfPermission(
+ reactContext,
+ Manifest.permission.POST_NOTIFICATIONS
+ ) != PackageManager.PERMISSION_GRANTED
+ ) {
+ activity?.let {
+ ActivityCompat.requestPermissions(
+ it,
+ arrayOf(Manifest.permission.POST_NOTIFICATIONS),
+ 101
+ )
+ }
+ }
+ }
+ }
+
}
diff --git a/android/src/main/java/com/amazonaws/ivs/reactnative/player/AmazonIvsViewManager.kt b/android/src/main/java/com/amazonaws/ivs/reactnative/player/AmazonIvsViewManager.kt
index f6beeb10..cab39143 100644
--- a/android/src/main/java/com/amazonaws/ivs/reactnative/player/AmazonIvsViewManager.kt
+++ b/android/src/main/java/com/amazonaws/ivs/reactnative/player/AmazonIvsViewManager.kt
@@ -184,11 +184,32 @@ class AmazonIvsViewManager : SimpleViewManager(),
override fun setProgressInterval(
view: AmazonIvsView?,
- value: Int
+ value: Double
) {
view?.setProgressInterval(value)
}
+ override fun setPlayInBackground(
+ view: AmazonIvsView?,
+ value: Boolean
+ ) {
+ view?.setPlayInBackground(value)
+ }
+
+ override fun setNotificationTitle(
+ view: AmazonIvsView?,
+ value: String?
+ ) {
+ view?.setNotificationTitle(value)
+ }
+
+ override fun setNotificationText(
+ view: AmazonIvsView?,
+ value: String?
+ ) {
+ view?.setNotificationText(value)
+ }
+
override fun preload(
view: AmazonIvsView?,
url: String,
diff --git a/android/src/main/java/com/amazonaws/ivs/reactnative/player/IVSBackgroundService.kt b/android/src/main/java/com/amazonaws/ivs/reactnative/player/IVSBackgroundService.kt
new file mode 100644
index 00000000..9eb17ae2
--- /dev/null
+++ b/android/src/main/java/com/amazonaws/ivs/reactnative/player/IVSBackgroundService.kt
@@ -0,0 +1,117 @@
+package com.amazonaws.ivs.reactnative.player
+
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.app.Service
+import android.content.Intent
+import android.os.Build
+import android.os.IBinder
+import android.util.Log
+import androidx.core.app.NotificationCompat
+import androidx.media.app.NotificationCompat.MediaStyle
+
+class IVSBackgroundService : Service() {
+
+ companion object {
+ const val CHANNEL_ID = "IVS_BACKGROUND_PLAYBACK_CHANNEL"
+ const val NOTIFICATION_ID = 101
+
+ const val ACTION_PLAY = "com.amazonaws.ivs.reactnative.player.PLAY"
+ const val ACTION_PAUSE = "com.amazonaws.ivs.reactnative.player.PAUSE"
+ const val ACTION_STOP = "com.amazonaws.ivs.reactnative.player.STOP"
+
+ const val EXTRA_IS_PLAYING = "IS_PLAYING"
+ const val NOTIFICATION_TITLE = "NOTIFICATION_TITLE"
+ const val NOTIFICATION_TEXT = "NOTIFICATION_TEXT"
+
+ }
+
+ override fun onBind(intent: Intent?): IBinder? = null
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ val action = intent?.action
+
+ when (action) {
+ ACTION_STOP -> {
+ stopForeground(true)
+ stopSelf()
+ return START_NOT_STICKY
+ }
+
+ ACTION_PLAY -> {
+ sendBroadcast(Intent("IVS_PLAYER_CONTROL").putExtra("action", "play"))
+ }
+
+ ACTION_PAUSE -> {
+ sendBroadcast(Intent("IVS_PLAYER_CONTROL").putExtra("action", "pause"))
+ }
+ }
+
+ val isPlaying = intent?.getBooleanExtra(EXTRA_IS_PLAYING, true) ?: true
+ val notificationTitle = intent?.getStringExtra(NOTIFICATION_TITLE) ?: ""
+ val notificationText = intent?.getStringExtra(NOTIFICATION_TEXT) ?: ""
+
+ createNotificationChannel()
+ startForeground(
+ NOTIFICATION_ID,
+ buildNotification(isPlaying, notificationTitle, notificationText)
+ )
+
+ return START_NOT_STICKY
+ }
+
+ private fun buildNotification(
+ isPlaying: Boolean,
+ notificationTitle: String,
+ notificationText: String
+ ): Notification {
+ val packageManager = applicationContext.packageManager
+ val launchIntent = packageManager.getLaunchIntentForPackage(applicationContext.packageName)
+ val pendingIntent = PendingIntent.getActivity(
+ this, 0, launchIntent,
+ PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+ )
+
+ val playIntent = Intent(this, IVSBackgroundService::class.java).apply { action = ACTION_PLAY }
+ val pauseIntent = Intent(this, IVSBackgroundService::class.java).apply { action = ACTION_PAUSE }
+
+ val pPlay = PendingIntent.getService(this, 1, playIntent, PendingIntent.FLAG_IMMUTABLE)
+ val pPause = PendingIntent.getService(this, 2, pauseIntent, PendingIntent.FLAG_IMMUTABLE)
+
+ val appIcon = applicationInfo.icon
+
+ val builder = NotificationCompat.Builder(this, CHANNEL_ID)
+ .setContentTitle(if (notificationTitle != "") notificationTitle else "Player")
+ .setContentText(if (notificationText != "") notificationText else if (isPlaying) "Playing" else "Paused")
+ .setSmallIcon(appIcon)
+ .setContentIntent(pendingIntent)
+ .setOngoing(isPlaying)
+ .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+ .setStyle(
+ MediaStyle()
+ .setShowActionsInCompactView(0)
+ )
+
+ if (isPlaying) {
+ builder.addAction(android.R.drawable.ic_media_pause, "Pause", pPause)
+ } else {
+ builder.addAction(android.R.drawable.ic_media_play, "Play", pPlay)
+ }
+
+ return builder.build()
+ }
+
+ private fun createNotificationChannel() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val serviceChannel = NotificationChannel(
+ CHANNEL_ID,
+ "Background Playback",
+ NotificationManager.IMPORTANCE_LOW
+ )
+ val manager = getSystemService(NotificationManager::class.java)
+ manager.createNotificationChannel(serviceChannel)
+ }
+ }
+}
diff --git a/docs/ivs-player-reference.md b/docs/ivs-player-reference.md
index df9e7914..b360457d 100644
--- a/docs/ivs-player-reference.md
+++ b/docs/ivs-player-reference.md
@@ -211,6 +211,8 @@ Value that specifies how often `onProgress` callback should be called in seconds
default: `1`
type: `number`
+min: `1`
+max: `5`
### onTimePoint _(optional)_
@@ -266,6 +268,51 @@ Value that enables picture in picture mode.
default: `undefined`
type: `boolean`
+> **Note:** To support Picture-in-Picture on iOS, you must enable **Background Modes** in your Xcode project settings.
+
+### playInBackground _(optional)_
+
+Value that enables stream playing in background.
+
+> **Note:** To support background play on iOS, you must enable **Background Modes** in your Xcode project settings.
+
+> **Note:** To enable background play on Android, you must add the following permissions to your `AndroidManifest.xml`:
+>
+> ```xml
+>
+>
+>
+>
+> ```
+
+default: `false`
+type: `boolean`
+
+### notificationTitle _(optional)_
+
+The main title displayed in the system notification tray when the app is running in the background.
+
+Android only
+
+default: `Player`
+type: `string`
+
+### notificationText _(optional)_
+
+The description text displayed in the system notification tray.
+
+Android only
+
+default: Dynamically changes based on state ("Playing" when active, "Paused" when inactive)
+type: `string`
+
+### showErrorMessage _(optional)_
+
+Show banner on error
+
+default: `undefined`
+type: `boolean`
+
### onPipChange _(optional)_
Callback that returns changes to the picture in picture state.
diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml
index 9a23fffd..d43f61a0 100644
--- a/example/android/app/src/main/AndroidManifest.xml
+++ b/example/android/app/src/main/AndroidManifest.xml
@@ -2,6 +2,10 @@
package="com.example.amazonivsreactnativeplayer">
+
+
+
+
(null);
const [detectedQuality, setDetectedQuality] = useState(null);
const [initialBufferDuration, setInitialBufferDuration] = useState(0.1);
@@ -95,15 +99,7 @@ export default function PlaygroundExample() {
const [resizeMode, setResizeMode] = useState(
RESIZE_MODES[1]
);
-
- useAppState({
- onBackground: () => {
- pauseInBackground && setPaused(true);
- },
- onForeground: () => {
- pauseInBackground && setPaused(false);
- },
- });
+ const [showErrorMessage, setShowErrorMessage] = useState(false);
const log = useCallback(
(text: string) => {
@@ -182,6 +178,8 @@ export default function PlaygroundExample() {
autoMaxQuality={autoMaxQuality}
breakpoints={breakpoints}
onSeek={(newPosition) => console.log('new position', newPosition)}
+ playInBackground={playInBackground}
+ notificationTitle="Playing in background"
onPlayerStateChange={(state) => {
if (state === PlayerState.Buffering) {
log(`buffering at ${detectedQuality?.name}`);
@@ -233,6 +231,7 @@ export default function PlaygroundExample() {
onVideoStatistics={(video) => console.log('onVideoStatistics', video)}
onError={(error) => console.log('error', error)}
onTimePoint={(timePoint) => console.log('time point', timePoint)}
+ showErrorMessage={showErrorMessage}
>
{orientation === Position.PORTRAIT ? (
<>
@@ -417,11 +416,17 @@ export default function PlaygroundExample() {
testID="rebufferToLive"
/>
+
;
@@ -279,6 +279,8 @@ export function TestPlan() {
function runplan() {
const plandata = parse(testPlan);
+ if (!plandata) return;
+
Object.keys(plandata).forEach((name) => {
const lname = name.toLowerCase();
const value = plandata[name];
diff --git a/example/src/useAppState.ts b/example/src/useAppState.ts
deleted file mode 100644
index 4c8063cb..00000000
--- a/example/src/useAppState.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { useState, useEffect } from 'react';
-import { AppState, AppStateStatus } from 'react-native';
-
-type Props = {
- onBackground?: () => void;
- onForeground?: () => void;
- onChange?: (status: AppStateStatus) => void;
-};
-
-export default function useAppState({
- onChange,
- onForeground,
- onBackground,
-}: Props) {
- const [appState, setAppState] = useState(AppState.currentState);
-
- useEffect(() => {
- function handleAppStateChange(nextAppState: AppStateStatus) {
- if (nextAppState === 'active' && appState !== 'active') {
- onForeground?.();
- } else if (
- appState === 'active' &&
- nextAppState.match(/inactive|background/)
- ) {
- onBackground?.();
- }
- setAppState(nextAppState);
- onChange?.(nextAppState);
- }
- const state = AppState.addEventListener('change', handleAppStateChange);
-
- return () => state.remove();
- }, [onChange, onForeground, onBackground, appState]);
-
- return { appState };
-}
diff --git a/ios/AmazonIvs.mm b/ios/AmazonIvs.mm
index fde5335f..f8ab2419 100644
--- a/ios/AmazonIvs.mm
+++ b/ios/AmazonIvs.mm
@@ -28,6 +28,12 @@ + (ComponentDescriptorProvider)componentDescriptorProvider {
return concreteComponentDescriptorProvider();
}
+// disable view recycling otherwise AV resourses fail to load on recycled view
++ (BOOL)shouldBeRecycled
+{
+ return NO;
+}
+
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
static const auto defaultProps = std::make_shared();
@@ -196,6 +202,10 @@ - (void)updateProps:(Props::Shared const &)props
if (oldViewProps.progressInterval != newViewProps.progressInterval) {
_ivsView.progressInterval = @(newViewProps.progressInterval);
}
+
+ if (oldViewProps.playInBackground != newViewProps.playInBackground) {
+ _ivsView.playInBackground = newViewProps.playInBackground;
+ }
[super updateProps:props oldProps:oldProps];
}
diff --git a/ios/AmazonIvsView.swift b/ios/AmazonIvsView.swift
index e540c268..d4eae042 100644
--- a/ios/AmazonIvsView.swift
+++ b/ios/AmazonIvsView.swift
@@ -38,6 +38,7 @@ import UIKit
private var _pipController: Any? = nil
private var isPipActive: Bool = false
+ private var wasPlayingBeforeBackground: Bool = false
@available(iOS 15, *)
private var pipController: AVPictureInPictureController? {
@@ -77,6 +78,35 @@ import UIKit
player.delegate = self
self.playerView.player = player
preparePictureInPicture()
+ addApplicationLifecycleObservers()
+ }
+
+ private func addApplicationLifecycleObservers() {
+ NotificationCenter.default.addObserver(
+ self,
+ selector: #selector(applicationDidEnterBackground(notification:)),
+ name: UIApplication.didEnterBackgroundNotification,
+ object: nil
+ )
+ NotificationCenter.default.addObserver(
+ self,
+ selector: #selector(applicationDidBecomeActive(notification:)),
+ name: UIApplication.didBecomeActiveNotification,
+ object: nil
+ )
+ }
+
+ private func removeApplicationLifecycleObservers() {
+ NotificationCenter.default.removeObserver(
+ self,
+ name: UIApplication.didEnterBackgroundNotification,
+ object: nil
+ )
+ NotificationCenter.default.removeObserver(
+ self,
+ name: UIApplication.didBecomeActiveNotification,
+ object: nil
+ )
}
deinit {
@@ -84,9 +114,14 @@ import UIKit
self.removeProgressObserver()
self.removePlayerObserver()
self.removeTimePointObserver()
+ self.removeApplicationLifecycleObservers()
}
func load(urlString: String) {
+ if self.playerView.player == nil {
+ self.playerView.player = self.player
+ }
+
finishedLoading = false
let url = URL(string: urlString)
self.onLoadStart?()
@@ -144,6 +179,18 @@ import UIKit
}
}
+ public var playInBackground: Bool = false {
+ didSet {
+ if playInBackground {
+ try? AVAudioSession.sharedInstance().setCategory(
+ .playback,
+ mode: .moviePlayback
+ )
+ try? AVAudioSession.sharedInstance().setActive(true)
+ }
+ }
+ }
+
public var pipEnabled: Bool {
didSet {
guard #available(iOS 15, *),
@@ -268,6 +315,11 @@ import UIKit
}
public func play() {
+ if UIApplication.shared.applicationState == .background
+ && pipEnabled == false
+ {
+ return
+ }
player.play()
}
@@ -531,6 +583,9 @@ import UIKit
public func player(_ player: IVSPlayer, didFailWithError error: Error) {
onError?(["error": error.localizedDescription])
+
+ player.pause()
+ self.playerView.player = nil
}
private func preparePictureInPicture() {
@@ -561,7 +616,36 @@ import UIKit
self.pipController = pipController
pipController.canStartPictureInPictureAutomaticallyFromInline =
self.pipEnabled
+ }
+
+ func applicationDidEnterBackground(notification: Notification) {
+ if isPipActive {
+ wasPlayingBeforeBackground = false
+ return
+ }
+
+ if playInBackground {
+ return
+ }
+
+ if player.state == .playing || player.state == .buffering {
+ wasPlayingBeforeBackground = true
+ pause()
+ } else {
+ wasPlayingBeforeBackground = false
+ }
+ }
+
+ func applicationDidBecomeActive(notification: Notification) {
+ if isPipActive {
+ return
+ }
+
+ if wasPlayingBeforeBackground {
+ play()
+ }
+ wasPlayingBeforeBackground = false
}
}
@available(iOS 15, *)
diff --git a/package.json b/package.json
index 1cd10c72..9dcc3f14 100644
--- a/package.json
+++ b/package.json
@@ -42,7 +42,7 @@
"postinstall": "husky",
"example": "yarn --cwd example",
"pods": "cd example && pod-install --quiet",
- "bootstrap": "yarn && yarn pods",
+ "bootstrap": "yarn && yarn pods && yarn markbuild",
"e2e:ios": "yarn bootstrap && yarn e2e:reset && yarn e2e:build:ios && yarn e2e:test:ios",
"e2e:android": "yarn bootstrap && yarn e2e:build:android && yarn e2e:test:android",
"e2e:reset": "detox clean-framework-cache && detox build-framework-cache",
diff --git a/src/__tests__/index.test.tsx b/src/__tests__/index.test.tsx
index 4fbaad96..4eea03d7 100644
--- a/src/__tests__/index.test.tsx
+++ b/src/__tests__/index.test.tsx
@@ -3,7 +3,7 @@ import React from 'react';
import { UIManager } from 'react-native';
import type { IVSPlayerRef } from '../types';
-import IVSPlayer from '../IVSPlayer';
+import IVSPlayer from '../components/IVSPlayer';
const URL =
'https://fcc3ddae59ed.us-west-2.playback.live-video.net/api/video/v1/us-west-2.893648527354.channel.DmumNckWFTqz.m3u8';
diff --git a/src/AmazonIvsViewNativeComponent.ts b/src/components/AmazonIvsViewNativeComponent.ts
similarity index 96%
rename from src/AmazonIvsViewNativeComponent.ts
rename to src/components/AmazonIvsViewNativeComponent.ts
index c538d89a..d07e550e 100644
--- a/src/AmazonIvsViewNativeComponent.ts
+++ b/src/components/AmazonIvsViewNativeComponent.ts
@@ -36,7 +36,10 @@ export interface NativeProps extends ViewProps {
maxBitrate?: Int32;
initialBufferDuration?: Double;
pipEnabled?: boolean;
- progressInterval?: Int32;
+ progressInterval?: Double;
+ playInBackground?: boolean;
+ notificationTitle?: string;
+ notificationText?: string;
onLoadStart?: DirectEventHandler<{}>;
onVideoStatistics?: DirectEventHandler<{
videoData: {
diff --git a/src/components/ErrorNotification.tsx b/src/components/ErrorNotification.tsx
new file mode 100644
index 00000000..97f27b05
--- /dev/null
+++ b/src/components/ErrorNotification.tsx
@@ -0,0 +1,70 @@
+import React, { useEffect, useRef, useState } from 'react';
+import { Animated, StyleSheet, Text } from 'react-native';
+
+interface ErrorNotificationProps {
+ message: string | null;
+}
+
+export const ErrorNotification = ({ message }: ErrorNotificationProps) => {
+ const [activeMessage, setActiveMessage] = useState(null);
+ const [showNotification, setShowNotification] = useState(false);
+ const animationValue = useRef(new Animated.Value(0)).current;
+
+ useEffect(() => {
+ if (message && message !== activeMessage) {
+ setActiveMessage(message);
+ setShowNotification(true);
+
+ Animated.timing(animationValue, {
+ toValue: 1,
+ duration: 300,
+ useNativeDriver: true,
+ }).start();
+ } else {
+ Animated.timing(animationValue, {
+ toValue: 0,
+ duration: 500,
+ useNativeDriver: true,
+ }).start(({ finished }) => {
+ if (finished) {
+ setShowNotification(false);
+ }
+ });
+ }
+ }, [message]);
+
+ if (!showNotification) return null;
+
+ return (
+
+ {activeMessage}
+
+ );
+};
+
+const styles = StyleSheet.create({
+ errorContainer: {
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ right: 0,
+ zIndex: 9999,
+ backgroundColor: '#ff4d4f',
+ padding: '16px',
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ errorText: {
+ color: '#fff',
+ fontSize: 14,
+ fontWeight: 600,
+ },
+});
diff --git a/src/IVSPlayer.tsx b/src/components/IVSPlayer.tsx
similarity index 87%
rename from src/IVSPlayer.tsx
rename to src/components/IVSPlayer.tsx
index 7b3f4702..8e2694d6 100644
--- a/src/IVSPlayer.tsx
+++ b/src/components/IVSPlayer.tsx
@@ -1,4 +1,10 @@
-import React, { useEffect, useImperativeHandle, useRef } from 'react';
+import React, {
+ useEffect,
+ useImperativeHandle,
+ useMemo,
+ useRef,
+ useState,
+} from 'react';
import {
Platform,
StyleSheet,
@@ -6,11 +12,6 @@ import {
type NativeSyntheticEvent,
type ViewStyle,
} from 'react-native';
-import AmazonIvsViewNativeComponent, {
- Commands,
-} from './AmazonIvsViewNativeComponent';
-import type { LogLevel, PlayerState } from './enums';
-import { createSourceWrapper } from './source';
import type {
IVSPlayerRef,
PlayerData,
@@ -20,7 +21,15 @@ import type {
TextCue,
TextMetadataCue,
VideoData,
-} from './types';
+} from '../types';
+import type { LogLevel, PlayerState } from '../types/enums';
+import { createSourceWrapper } from '../types/source';
+import AmazonIvsViewNativeComponent, {
+ Commands,
+} from './AmazonIvsViewNativeComponent';
+import { ErrorNotification } from './ErrorNotification';
+
+const MAX_PROGRESS_INTERVAL = 5;
export type Props = {
style?: ViewStyle;
@@ -44,6 +53,10 @@ export type Props = {
maxBitrate?: number;
initialBufferDuration?: number;
pipEnabled?: boolean;
+ showErrorMessage?: boolean;
+ playInBackground?: boolean;
+ notificationTitle?: string;
+ notificationText?: string;
onSeek?(position: number): void;
onData?(data: PlayerData): void;
onVideoStatistics?(data: VideoData): void;
@@ -85,7 +98,7 @@ const IVSPlayerContainer = React.forwardRef(
playbackRate,
pipEnabled,
logLevel,
- progressInterval,
+ progressInterval = 1,
volume,
quality,
autoMaxQuality,
@@ -93,6 +106,10 @@ const IVSPlayerContainer = React.forwardRef(
breakpoints = [],
maxBitrate,
initialBufferDuration,
+ showErrorMessage,
+ playInBackground = false,
+ notificationTitle,
+ notificationText,
onSeek,
onData,
onVideoStatistics,
@@ -115,6 +132,7 @@ const IVSPlayerContainer = React.forwardRef(
) => {
const mediaPlayerRef = useRef(null);
const initialized = useRef(false);
+ const [errorMessage, setErrorMessage] = useState();
const preload = (url: string) => {
if (!mediaPlayerRef.current || !url) return null;
@@ -298,6 +316,13 @@ const IVSPlayerContainer = React.forwardRef(
const onErrorHandler = (event: NativeSyntheticEvent<{ error: string }>) => {
const { error } = event.nativeEvent;
+
+ if (showErrorMessage) {
+ setErrorMessage(error);
+ setTimeout(() => {
+ setErrorMessage(null);
+ }, 5000);
+ }
onError?.(error);
};
@@ -308,8 +333,17 @@ const IVSPlayerContainer = React.forwardRef(
onTimePoint?.(position);
};
+ const constrainedProgressInterval = useMemo(() => {
+ if (!progressInterval || progressInterval <= 0.1) {
+ return 0.1;
+ }
+
+ return Math.min(progressInterval, MAX_PROGRESS_INTERVAL);
+ }, [progressInterval]);
+
return (
+
(
streamUrl={streamUrl}
logLevel={logLevel}
resizeMode={resizeMode}
- progressInterval={progressInterval}
+ progressInterval={constrainedProgressInterval}
volume={volume}
quality={quality}
initialBufferDuration={initialBufferDuration}
@@ -331,6 +365,9 @@ const IVSPlayerContainer = React.forwardRef(
breakpoints={breakpoints}
maxBitrate={maxBitrate}
pipEnabled={pipEnabled}
+ playInBackground={playInBackground}
+ notificationTitle={notificationTitle}
+ notificationText={notificationText}
onVideoStatistics={onVideoStatisticsHandler}
onData={onDataHandler}
onSeek={onSeekHandler}
diff --git a/src/index.ts b/src/index.ts
index 40f1c834..416b7096 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,6 +1,6 @@
-export * from './AmazonIvsViewNativeComponent';
-export { default as AmazonIvsView } from './AmazonIvsViewNativeComponent';
-export * from './enums';
-export { default } from './IVSPlayer';
-export type { Props as IVSPlayerProps } from './IVSPlayer';
+export * from './components/AmazonIvsViewNativeComponent';
+export { default as AmazonIvsView } from './components/AmazonIvsViewNativeComponent';
+export { default } from './components/IVSPlayer';
+export type { Props as IVSPlayerProps } from './components/IVSPlayer';
export * from './types';
+export * from './types/enums';
diff --git a/src/enums.ts b/src/types/enums.ts
similarity index 100%
rename from src/enums.ts
rename to src/types/enums.ts
diff --git a/src/types.ts b/src/types/index.ts
similarity index 100%
rename from src/types.ts
rename to src/types/index.ts
diff --git a/src/source.ts b/src/types/source.ts
similarity index 91%
rename from src/source.ts
rename to src/types/source.ts
index d77826ec..ffa61b8e 100644
--- a/src/source.ts
+++ b/src/types/source.ts
@@ -1,4 +1,4 @@
-import type { Source } from './types';
+import type { Source } from '.';
let sourceId = 0;