diff --git a/apps/native-component-list/src/screens/Video/VideoScreen.tsx b/apps/native-component-list/src/screens/Video/VideoScreen.tsx index 3ac652dc5b70e..5fbae08a08e16 100644 --- a/apps/native-component-list/src/screens/Video/VideoScreen.tsx +++ b/apps/native-component-list/src/screens/Video/VideoScreen.tsx @@ -2,7 +2,7 @@ import Slider from '@react-native-community/slider'; import { Picker } from '@react-native-picker/picker'; import SegmentedControl from '@react-native-segmented-control/segmented-control'; import { Platform } from 'expo-modules-core'; -import { useVideoPlayer, VideoView, VideoSource } from 'expo-video'; +import { useVideoPlayer, VideoView, VideoSource, VideoPlayerEvents } from 'expo-video'; import React, { useCallback, useEffect, useRef } from 'react'; import { PixelRatio, ScrollView, StyleSheet, Text, View } from 'react-native'; @@ -30,7 +30,14 @@ const androidDrmSource: VideoSource = { const videoLabels: string[] = ['Big Buck Bunny', 'Elephants Dream']; const videoSources: VideoSource[] = [bigBuckBunnySource, elephantsDreamSource]; const playbackRates: number[] = [0.25, 0.5, 1, 1.5, 2, 16]; - +const eventsToListen: (keyof VideoPlayerEvents)[] = [ + 'statusChange', + 'playingChange', + 'playbackRateChange', + 'volumeChange', + 'playToEnd', + 'sourceChange', +]; if (Platform.OS === 'android') { videoLabels.push('Tears of Steel (DRM protected)'); videoSources.push(androidDrmSource); @@ -47,11 +54,18 @@ export default function VideoScreen() { const [staysActiveInBackground, setStaysActiveInBackground] = React.useState(false); const [loop, setLoop] = React.useState(false); const [playbackRateIndex, setPlaybackRateIndex] = React.useState(2); - const [shouldCorrectPitch, setCorrectsPitch] = React.useState(true); + const [preservePitch, setPreservePitch] = React.useState(true); const [volume, setVolume] = React.useState(1); const [currentSource, setCurrentSource] = React.useState(videoSources[0]); + const [logEvents, setLogEvents] = React.useState(false); - const player = useVideoPlayer(currentSource); + const player = useVideoPlayer(currentSource, (player) => { + player.volume = volume; + player.loop = loop; + player.preservesPitch = preservePitch; + player.staysActiveInBackground = staysActiveInBackground; + player.play(); + }); const enterFullscreen = useCallback(() => { ref.current?.enterFullscreen(); @@ -102,17 +116,30 @@ export default function VideoScreen() { ); const updatePreservesPitch = useCallback( - (correctPitch: boolean) => { - player.preservesPitch = correctPitch; - setCorrectsPitch(correctPitch); + (preservesPitch: boolean) => { + player.preservesPitch = preservesPitch; + setPreservePitch(preservesPitch); }, [player] ); useEffect(() => { - player.play(); - player.preservesPitch = shouldCorrectPitch; - }, [player]); + if (logEvents) { + eventsToListen.forEach((eventName) => { + player.addListener(eventName, (newValue: any, _: any, error: any) => { + console.log( + `${eventName}: ${JSON.stringify(newValue)} ${(error && JSON.stringify(error)) ?? ''}` + ); + }); + }); + } + + return () => { + eventsToListen.forEach((eventName) => { + player.removeAllListeners(eventName); + }); + }; + }, [logEvents, player]); return ( @@ -234,11 +261,18 @@ export default function VideoScreen() { + diff --git a/packages/expo-modules-core/android/src/main/java/expo/modules/kotlin/sharedobjects/SharedObject.kt b/packages/expo-modules-core/android/src/main/java/expo/modules/kotlin/sharedobjects/SharedObject.kt index 214db20923fa8..520934a485bc3 100644 --- a/packages/expo-modules-core/android/src/main/java/expo/modules/kotlin/sharedobjects/SharedObject.kt +++ b/packages/expo-modules-core/android/src/main/java/expo/modules/kotlin/sharedobjects/SharedObject.kt @@ -26,12 +26,12 @@ open class SharedObject(appContext: AppContext? = null) { ) } - fun sendEvent(eventName: String, vararg args: Any) { + fun sendEvent(eventName: String, vararg args: Any?) { val jsThis = getJavaScriptObject() ?: return try { jsThis.getProperty("emit") - .getFunction() + .getFunction() .invoke( eventName, *args, diff --git a/packages/expo-video/CHANGELOG.md b/packages/expo-video/CHANGELOG.md index baffcc06d342b..b6441d9e3f93d 100644 --- a/packages/expo-video/CHANGELOG.md +++ b/packages/expo-video/CHANGELOG.md @@ -6,6 +6,7 @@ ### 🎉 New features +- Add support for events on Android and iOS. ([#27632](https://github.com/expo/expo/pull/27632) by [@behenate](https://github.com/behenate)) - Add support for `loop`, `playbackRate`, `preservesPitch` and `currentTime` properties. ([#27367](https://github.com/expo/expo/pull/27367) by [@behenate](https://github.com/behenate)) - Add background playback support. ([#27110](https://github.com/expo/expo/pull/27110) by [@behenate](https://github.com/behenate)) - Add DRM support for Android and iOS. ([#26465](https://github.com/expo/expo/pull/26465) by [@behenate](https://github.com/behenate)) diff --git a/packages/expo-video/android/src/main/java/expo/modules/video/VideoExceptions.kt b/packages/expo-video/android/src/main/java/expo/modules/video/VideoExceptions.kt index 38e24b157e64e..acc996cfcdcc0 100644 --- a/packages/expo-video/android/src/main/java/expo/modules/video/VideoExceptions.kt +++ b/packages/expo-video/android/src/main/java/expo/modules/video/VideoExceptions.kt @@ -20,3 +20,6 @@ internal class PictureInPictureUnsupportedException : internal class UnsupportedDRMTypeException(type: DRMType) : CodedException("DRM type `$type` is not supported on Android") + +internal class PlaybackException(reason: String?, cause: Throwable? = null) : + CodedException("A playback exception has occurred: ${reason ?: "reason unknown"}", cause) diff --git a/packages/expo-video/android/src/main/java/expo/modules/video/VideoManager.kt b/packages/expo-video/android/src/main/java/expo/modules/video/VideoManager.kt index 1a7db4bef0e30..4be43a09ca110 100644 --- a/packages/expo-video/android/src/main/java/expo/modules/video/VideoManager.kt +++ b/packages/expo-video/android/src/main/java/expo/modules/video/VideoManager.kt @@ -1,7 +1,10 @@ package expo.modules.video import androidx.annotation.OptIn +import androidx.media3.common.MediaItem import androidx.media3.common.util.UnstableApi +import expo.modules.video.records.VideoSource +import java.lang.ref.WeakReference // Helper class used to keep track of all existing VideoViews and VideoPlayers @OptIn(UnstableApi::class) @@ -14,6 +17,9 @@ object VideoManager { // Keeps track of all existing VideoPlayers, and whether they are attached to a VideoView private var videoPlayersToVideoViews = mutableMapOf>() + // Keeps track of all existing MediaItems and their corresponding VideoSources. Used for recognizing source of MediaItems. + private var mediaItemsToVideoSources = mutableMapOf>() + fun registerVideoView(videoView: VideoView) { videoViews[videoView.id] = videoView } @@ -34,6 +40,17 @@ object VideoManager { videoPlayersToVideoViews.remove(videoPlayer) } + fun registerVideoSourceToMediaItem(mediaItem: MediaItem, videoSource: VideoSource) { + mediaItemsToVideoSources[mediaItem.mediaId] = WeakReference(videoSource) + } + + fun getVideoSourceFromMediaItem(mediaItem: MediaItem?): VideoSource? { + if (mediaItem == null) { + return null + } + return mediaItemsToVideoSources[mediaItem.mediaId]?.get() + } + fun onVideoPlayerAttachedToView(videoPlayer: VideoPlayer, videoView: VideoView) { if (videoPlayersToVideoViews[videoPlayer]?.contains(videoView) == true) { return diff --git a/packages/expo-video/android/src/main/java/expo/modules/video/VideoModule.kt b/packages/expo-video/android/src/main/java/expo/modules/video/VideoModule.kt index 26a3367b4f5ed..428ad14490d75 100644 --- a/packages/expo-video/android/src/main/java/expo/modules/video/VideoModule.kt +++ b/packages/expo-video/android/src/main/java/expo/modules/video/VideoModule.kt @@ -139,7 +139,9 @@ class VideoModule : Module() { Class(VideoPlayer::class) { Constructor { source: VideoSource -> - VideoPlayer(activity.applicationContext, appContext, source.toMediaItem()) + val mediaItem = source.toMediaItem() + VideoManager.registerVideoSourceToMediaItem(mediaItem, source) + VideoPlayer(activity.applicationContext, appContext, mediaItem) } Property("playing") @@ -147,11 +149,6 @@ class VideoModule : Module() { ref.playing } - Property("isLoading") - .get { ref: VideoPlayer -> - ref.isLoading - } - Property("muted") .get { ref: VideoPlayer -> ref.muted @@ -209,6 +206,11 @@ class VideoModule : Module() { } } + Property("status") + .get { ref: VideoPlayer -> + ref.status + } + Property("staysActiveInBackground") .get { ref: VideoPlayer -> ref.staysActiveInBackground @@ -249,9 +251,11 @@ class VideoModule : Module() { } else { VideoSource(source.get(String::class)) } + val mediaItem = videoSource.toMediaItem() + VideoManager.registerVideoSourceToMediaItem(mediaItem, videoSource) appContext.mainQueue.launch { - ref.player.setMediaItem(videoSource.toMediaItem()) + ref.player.setMediaItem(mediaItem) } } diff --git a/packages/expo-video/android/src/main/java/expo/modules/video/VideoPlayer.kt b/packages/expo-video/android/src/main/java/expo/modules/video/VideoPlayer.kt index 7c015f695a4a6..f4e29e4675b57 100644 --- a/packages/expo-video/android/src/main/java/expo/modules/video/VideoPlayer.kt +++ b/packages/expo-video/android/src/main/java/expo/modules/video/VideoPlayer.kt @@ -10,6 +10,7 @@ import android.os.IBinder import android.util.Log import android.view.SurfaceView import androidx.media3.common.MediaItem +import androidx.media3.common.PlaybackException import androidx.media3.common.PlaybackParameters import androidx.media3.common.Player import androidx.media3.common.Timeline @@ -20,6 +21,10 @@ import androidx.media3.session.MediaSessionService import androidx.media3.ui.PlayerView import expo.modules.kotlin.AppContext import expo.modules.kotlin.sharedobjects.SharedObject +import expo.modules.video.enums.PlayerStatus +import expo.modules.video.enums.PlayerStatus.* +import expo.modules.video.records.PlaybackError +import expo.modules.video.records.VolumeEvent import kotlinx.coroutines.launch // https://developer.android.com/guide/topics/media/media3/getting-started/migration-guide#improvements_in_media3 @@ -36,7 +41,25 @@ class VideoPlayer(context: Context, appContext: AppContext, private val mediaIte // We duplicate some properties of the player, because we don't want to always use the mainQueue to access them. var playing = false - var isLoading = true + set(value) { + if (field != value) { + sendEventOnJSThread("playingChange", value, field) + } + field = value + } + + var status: PlayerStatus = IDLE + + var currentMediaItem: MediaItem? = null + set(newMediaItem) { + if (field != newMediaItem) { + val oldVideoSource = VideoManager.getVideoSourceFromMediaItem(field) + val newVideoSource = VideoManager.getVideoSourceFromMediaItem(newMediaItem) + + sendEventOnJSThread("sourceChange", newVideoSource, oldVideoSource) + } + field = newMediaItem + } // Volume of the player if there was no mute applied. var userVolume = 1f @@ -44,7 +67,7 @@ class VideoPlayer(context: Context, appContext: AppContext, private val mediaIte var staysActiveInBackground = false var preservesPitch = false set(preservesPitch) { - applyPitchCorrection() + playbackParameters = applyPitchCorrection(playbackParameters) field = preservesPitch } @@ -56,21 +79,29 @@ class VideoPlayer(context: Context, appContext: AppContext, private val mediaIte set(volume) { if (player.volume == volume) return player.volume = if (muted) 0f else volume + sendEventOnJSThread("volumeChange", VolumeEvent(volume, muted), VolumeEvent(field, muted)) field = volume } var muted = false set(muted) { + if (field == muted) return + sendEventOnJSThread("volumeChange", VolumeEvent(volume, muted), VolumeEvent(volume, field)) + player.volume = if (muted) 0f else userVolume field = muted - volume = if (muted) 0f else userVolume } var playbackParameters: PlaybackParameters = PlaybackParameters.DEFAULT - set(value) { - if (player.playbackParameters == value) return - player.playbackParameters = value - field = value - applyPitchCorrection() + set(newPlaybackParameters) { + if (playbackParameters.speed != newPlaybackParameters.speed) { + sendEventOnJSThread("playbackRateChange", newPlaybackParameters.speed, playbackParameters.speed) + } + val pitchCorrectedPlaybackParameters = applyPitchCorrection(newPlaybackParameters) + field = pitchCorrectedPlaybackParameters + + if (player.playbackParameters != pitchCorrectedPlaybackParameters) { + player.playbackParameters = pitchCorrectedPlaybackParameters + } } private val playerListener = object : Player.Listener { @@ -82,8 +113,20 @@ class VideoPlayer(context: Context, appContext: AppContext, private val mediaIte this@VideoPlayer.timeline = timeline } - override fun onIsLoadingChanged(isLoading: Boolean) { - this@VideoPlayer.isLoading = isLoading + override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { + this@VideoPlayer.currentMediaItem = mediaItem + if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_REPEAT) { + sendEventOnJSThread("playToEnd") + } + super.onMediaItemTransition(mediaItem, reason) + } + + override fun onPlaybackStateChanged(@Player.State playbackState: Int) { + if (playbackState == Player.STATE_IDLE && player.playerError != null) { + return + } + setStatus(playerStateToPlayerStatus(playbackState), null) + super.onPlaybackStateChanged(playbackState) } override fun onVolumeChanged(volume: Float) { @@ -94,6 +137,16 @@ class VideoPlayer(context: Context, appContext: AppContext, private val mediaIte this@VideoPlayer.playbackParameters = playbackParameters super.onPlaybackParametersChanged(playbackParameters) } + + override fun onPlayerErrorChanged(error: PlaybackException?) { + error?.let { + setStatus(ERROR, error) + } ?: run { + setStatus(playerStateToPlayerStatus(player.playbackState), null) + } + + super.onPlayerErrorChanged(error) + } } init { @@ -167,9 +220,47 @@ class VideoPlayer(context: Context, appContext: AppContext, private val mediaIte player.prepare() } - private fun applyPitchCorrection() { + private fun applyPitchCorrection(playbackParameters: PlaybackParameters): PlaybackParameters { val speed = playbackParameters.speed val pitch = if (preservesPitch) 1f else speed - playbackParameters = PlaybackParameters(speed, pitch) + return PlaybackParameters(speed, pitch) + } + + private fun playerStateToPlayerStatus(@Player.State state: Int): PlayerStatus { + return when (state) { + Player.STATE_IDLE -> IDLE + Player.STATE_BUFFERING -> LOADING + Player.STATE_READY -> READY_TO_PLAY + Player.STATE_ENDED -> { + // When an error occurs, the player state changes to ENDED. + if (player.playerError != null) { + ERROR + } else { + IDLE + } + } + + else -> IDLE + } + } + private fun setStatus(status: PlayerStatus, error: PlaybackException?) { + val playbackError = error?.let { + PlaybackError(it) + } + + if (playbackError == null && player.playbackState == Player.STATE_ENDED) { + sendEventOnJSThread("playToEnd") + } + + if (this.status != status) { + sendEventOnJSThread("statusChange", status.value, this.status.value, playbackError) + } + this.status = status + } + + private fun sendEventOnJSThread(eventName: String, vararg args: Any?) { + appContext?.executeOnJavaScriptThread { + sendEvent(eventName, *args) + } } } diff --git a/packages/expo-video/android/src/main/java/expo/modules/video/enums/DRMType.kt b/packages/expo-video/android/src/main/java/expo/modules/video/enums/DRMType.kt index 7b44995e3abbc..c97e5ac61c601 100644 --- a/packages/expo-video/android/src/main/java/expo/modules/video/enums/DRMType.kt +++ b/packages/expo-video/android/src/main/java/expo/modules/video/enums/DRMType.kt @@ -5,7 +5,7 @@ import expo.modules.kotlin.types.Enumerable import expo.modules.video.UnsupportedDRMTypeException import java.util.UUID -internal enum class DRMType(val value: String) : Enumerable { +enum class DRMType(val value: String) : Enumerable { CLEARKEY("clearkey"), FAIRPLAY("fairplay"), PLAYREADY("playready"), diff --git a/packages/expo-video/android/src/main/java/expo/modules/video/enums/PlayerStatus.kt b/packages/expo-video/android/src/main/java/expo/modules/video/enums/PlayerStatus.kt new file mode 100644 index 0000000000000..c57dee032b8ef --- /dev/null +++ b/packages/expo-video/android/src/main/java/expo/modules/video/enums/PlayerStatus.kt @@ -0,0 +1,10 @@ +package expo.modules.video.enums + +import expo.modules.kotlin.types.Enumerable + +enum class PlayerStatus(val value: String) : Enumerable { + IDLE("idle"), + LOADING("loading"), + READY_TO_PLAY("readyToPlay"), + ERROR("error") +} diff --git a/packages/expo-video/android/src/main/java/expo/modules/video/records/DRMOptions.kt b/packages/expo-video/android/src/main/java/expo/modules/video/records/DRMOptions.kt index e50fdebff710e..f01c7f02e4b96 100644 --- a/packages/expo-video/android/src/main/java/expo/modules/video/records/DRMOptions.kt +++ b/packages/expo-video/android/src/main/java/expo/modules/video/records/DRMOptions.kt @@ -6,7 +6,7 @@ import expo.modules.kotlin.records.Record import expo.modules.video.enums.DRMType import java.io.Serializable -internal class DRMOptions( +class DRMOptions( @Field var type: DRMType = DRMType.WIDEVINE, @Field var licenseServer: String? = null, @Field var headers: Map? = null, diff --git a/packages/expo-video/android/src/main/java/expo/modules/video/records/PlaybackError.kt b/packages/expo-video/android/src/main/java/expo/modules/video/records/PlaybackError.kt new file mode 100644 index 0000000000000..6f226fb0da7f5 --- /dev/null +++ b/packages/expo-video/android/src/main/java/expo/modules/video/records/PlaybackError.kt @@ -0,0 +1,19 @@ +package expo.modules.video.records + +import androidx.media3.common.PlaybackException +import expo.modules.kotlin.records.Field +import expo.modules.kotlin.records.Record +import java.io.Serializable + +class PlaybackError( + @Field var message: String? = null +) : Record, Serializable { + constructor(exception: PlaybackException) : this(errorMessageFromException(exception)) + + companion object { + private fun errorMessageFromException(exception: PlaybackException): String { + val reason = "${exception.localizedMessage} ${exception.cause?.localizedMessage ?: ""}" + return "A playback exception has occurred: $reason" + } + } +} diff --git a/packages/expo-video/android/src/main/java/expo/modules/video/records/VideoSource.kt b/packages/expo-video/android/src/main/java/expo/modules/video/records/VideoSource.kt index a799f9c2531c3..d0fd7f9ae711a 100644 --- a/packages/expo-video/android/src/main/java/expo/modules/video/records/VideoSource.kt +++ b/packages/expo-video/android/src/main/java/expo/modules/video/records/VideoSource.kt @@ -6,10 +6,19 @@ import expo.modules.kotlin.records.Record import expo.modules.video.UnsupportedDRMTypeException import java.io.Serializable -internal class VideoSource( +class VideoSource( @Field var uri: String? = null, @Field var drm: DRMOptions? = null ) : Record, Serializable { + private fun toMediaId(): String { + return "uri:${this.uri}" + + "DrmType:${this.drm?.type}" + + "DrmLicenseServer:${this.drm?.licenseServer}" + + "DrmMultiKey:${this.drm?.multiKey}" + + "DRMHeadersKeys:${this.drm?.headers?.keys?.joinToString {it}}}" + + "DRMHeadersValues:${this.drm?.headers?.values?.joinToString {it}}}" + } + fun toMediaItem() = MediaItem .Builder() .apply { diff --git a/packages/expo-video/android/src/main/java/expo/modules/video/records/VolumeEvent.kt b/packages/expo-video/android/src/main/java/expo/modules/video/records/VolumeEvent.kt new file mode 100644 index 0000000000000..0e25af1c6b398 --- /dev/null +++ b/packages/expo-video/android/src/main/java/expo/modules/video/records/VolumeEvent.kt @@ -0,0 +1,10 @@ +package expo.modules.video.records + +import expo.modules.kotlin.records.Field +import expo.modules.kotlin.records.Record +import java.io.Serializable + +internal class VolumeEvent( + @Field var volume: Float? = null, + @Field var muted: Boolean? = null +) : Record, Serializable diff --git a/packages/expo-video/build/NativeVideoModule.web.d.ts b/packages/expo-video/build/NativeVideoModule.web.d.ts new file mode 100644 index 0000000000000..a5545849f1af0 --- /dev/null +++ b/packages/expo-video/build/NativeVideoModule.web.d.ts @@ -0,0 +1,3 @@ +declare const _default: () => void; +export default _default; +//# sourceMappingURL=NativeVideoModule.web.d.ts.map \ No newline at end of file diff --git a/packages/expo-video/build/NativeVideoModule.web.d.ts.map b/packages/expo-video/build/NativeVideoModule.web.d.ts.map new file mode 100644 index 0000000000000..4b48022f87b79 --- /dev/null +++ b/packages/expo-video/build/NativeVideoModule.web.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"NativeVideoModule.web.d.ts","sourceRoot":"","sources":["../src/NativeVideoModule.web.ts"],"names":[],"mappings":";AAAA,wBAAwB"} \ No newline at end of file diff --git a/packages/expo-video/build/NativeVideoModule.web.js b/packages/expo-video/build/NativeVideoModule.web.js new file mode 100644 index 0000000000000..f2c1d2eb52c98 --- /dev/null +++ b/packages/expo-video/build/NativeVideoModule.web.js @@ -0,0 +1,2 @@ +export default () => { }; +//# sourceMappingURL=NativeVideoModule.web.js.map \ No newline at end of file diff --git a/packages/expo-video/build/NativeVideoModule.web.js.map b/packages/expo-video/build/NativeVideoModule.web.js.map new file mode 100644 index 0000000000000..ee9770038b9ed --- /dev/null +++ b/packages/expo-video/build/NativeVideoModule.web.js.map @@ -0,0 +1 @@ +{"version":3,"file":"NativeVideoModule.web.js","sourceRoot":"","sources":["../src/NativeVideoModule.web.ts"],"names":[],"mappings":"AAAA,eAAe,GAAG,EAAE,GAAE,CAAC,CAAC","sourcesContent":["export default () => {};\n"]} \ No newline at end of file diff --git a/packages/expo-video/build/VideoView.d.ts b/packages/expo-video/build/VideoView.d.ts index 09899951611dd..dd4d18face94e 100644 --- a/packages/expo-video/build/VideoView.d.ts +++ b/packages/expo-video/build/VideoView.d.ts @@ -1,6 +1,6 @@ import { ReactNode, PureComponent } from 'react'; import { VideoPlayer, VideoSource, VideoViewProps } from './VideoView.types'; -export declare function useVideoPlayer(source: VideoSource): VideoPlayer; +export declare function useVideoPlayer(source: VideoSource, setup?: (player: VideoPlayer) => void): VideoPlayer; /** * Returns whether the current device supports Picture in Picture (PiP) mode. * @returns A `boolean` which is `true` if the device supports PiP mode, and `false` otherwise. diff --git a/packages/expo-video/build/VideoView.d.ts.map b/packages/expo-video/build/VideoView.d.ts.map index 724781ee98213..30900ea2ccad4 100644 --- a/packages/expo-video/build/VideoView.d.ts.map +++ b/packages/expo-video/build/VideoView.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"VideoView.d.ts","sourceRoot":"","sources":["../src/VideoView.tsx"],"names":[],"mappings":"AACA,OAAO,EACL,SAAS,EACT,aAAa,EAMd,MAAM,OAAO,CAAC;AAIf,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAE7E,wBAAgB,cAAc,CAAC,MAAM,EAAE,WAAW,GAAG,WAAW,CAO/D;AAED;;;;;GAKG;AACH,wBAAgB,2BAA2B,IAAI,OAAO,CAAC,OAAO,CAAC,CAE9D;AAED,qBAAa,SAAU,SAAQ,aAAa,CAAC,cAAc,CAAC;IAC1D,SAAS,iCAAoB;IAE7B,OAAO,CAAC,MAAM,EAAE,WAAW;IAQ3B,eAAe;IAIf,cAAc;IAId;;;;;OAKG;IACH,qBAAqB;IAIrB;;;;OAIG;IACH,oBAAoB;IAIpB,MAAM,IAAI,SAAS;CAMpB"} \ No newline at end of file +{"version":3,"file":"VideoView.d.ts","sourceRoot":"","sources":["../src/VideoView.tsx"],"names":[],"mappings":"AACA,OAAO,EACL,SAAS,EACT,aAAa,EAMd,MAAM,OAAO,CAAC;AAIf,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAE7E,wBAAgB,cAAc,CAC5B,MAAM,EAAE,WAAW,EACnB,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,GACpC,WAAW,CAQb;AAED;;;;;GAKG;AACH,wBAAgB,2BAA2B,IAAI,OAAO,CAAC,OAAO,CAAC,CAE9D;AAED,qBAAa,SAAU,SAAQ,aAAa,CAAC,cAAc,CAAC;IAC1D,SAAS,iCAAoB;IAE7B,OAAO,CAAC,MAAM,EAAE,WAAW;IAQ3B,eAAe;IAIf,cAAc;IAId;;;;;OAKG;IACH,qBAAqB;IAIrB;;;;OAIG;IACH,oBAAoB;IAIpB,MAAM,IAAI,SAAS;CAMpB"} \ No newline at end of file diff --git a/packages/expo-video/build/VideoView.js b/packages/expo-video/build/VideoView.js index e3bc2a9177dd9..2f7b6adc4773c 100644 --- a/packages/expo-video/build/VideoView.js +++ b/packages/expo-video/build/VideoView.js @@ -1,9 +1,13 @@ import { PureComponent, createRef, useRef, useMemo, useEffect, } from 'react'; import NativeVideoModule from './NativeVideoModule'; import NativeVideoView from './NativeVideoView'; -export function useVideoPlayer(source) { +export function useVideoPlayer(source, setup) { const parsedSource = typeof source === 'string' ? { uri: source } : source; - return useReleasingSharedObject(() => new NativeVideoModule.VideoPlayer(parsedSource), [JSON.stringify(parsedSource)]); + return useReleasingSharedObject(() => { + const player = new NativeVideoModule.VideoPlayer(parsedSource); + setup?.(player); + return player; + }, [JSON.stringify(parsedSource)]); } /** * Returns whether the current device supports Picture in Picture (PiP) mode. diff --git a/packages/expo-video/build/VideoView.js.map b/packages/expo-video/build/VideoView.js.map index 66ecf80f87c68..bb292fa4bbf4f 100644 --- a/packages/expo-video/build/VideoView.js.map +++ b/packages/expo-video/build/VideoView.js.map @@ -1 +1 @@ -{"version":3,"file":"VideoView.js","sourceRoot":"","sources":["../src/VideoView.tsx"],"names":[],"mappings":"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","sourcesContent":["import { SharedObject } from 'expo-modules-core';\nimport {\n ReactNode,\n PureComponent,\n DependencyList,\n createRef,\n useRef,\n useMemo,\n useEffect,\n} from 'react';\n\nimport NativeVideoModule from './NativeVideoModule';\nimport NativeVideoView from './NativeVideoView';\nimport { VideoPlayer, VideoSource, VideoViewProps } from './VideoView.types';\n\nexport function useVideoPlayer(source: VideoSource): VideoPlayer {\n const parsedSource = typeof source === 'string' ? { uri: source } : source;\n\n return useReleasingSharedObject(\n () => new NativeVideoModule.VideoPlayer(parsedSource),\n [JSON.stringify(parsedSource)]\n );\n}\n\n/**\n * Returns whether the current device supports Picture in Picture (PiP) mode.\n * @returns A `boolean` which is `true` if the device supports PiP mode, and `false` otherwise.\n * @platform android\n * @platform ios\n */\nexport function isPictureInPictureSupported(): Promise {\n return NativeVideoModule.isPictureInPictureSupported();\n}\n\nexport class VideoView extends PureComponent {\n nativeRef = createRef();\n\n replace(source: VideoSource) {\n if (typeof source === 'string') {\n this.nativeRef.current?.replace({ uri: source });\n return;\n }\n this.nativeRef.current?.replace(source);\n }\n\n enterFullscreen() {\n this.nativeRef.current?.enterFullscreen();\n }\n\n exitFullscreen() {\n this.nativeRef.current?.exitFullscreen();\n }\n\n /**\n * Enters Picture in Picture (PiP) mode. Throws an exception if the device does not support PiP.\n * > **Note:** Only one player can be in Picture in Picture (PiP) mode at a time.\n * @platform android\n * @platform ios 14+\n */\n startPictureInPicture() {\n return this.nativeRef.current?.startPictureInPicture();\n }\n\n /**\n * Exits Picture in Picture (PiP) mode.\n * @platform android\n * @platform ios 14+\n */\n stopPictureInPicture() {\n return this.nativeRef.current?.stopPictureInPicture();\n }\n\n render(): ReactNode {\n const { player, ...props } = this.props;\n const playerId = getPlayerId(player);\n\n return ;\n }\n}\n\n// Temporary solution to pass the shared object ID instead of the player object.\n// We can't really pass it as an object in the old architecture.\n// Technically we can in the new architecture, but it's not possible yet.\nfunction getPlayerId(player: number | VideoPlayer): number | null {\n if (player instanceof NativeVideoModule.VideoPlayer) {\n // @ts-expect-error\n return player.__expo_shared_object_id__;\n }\n if (typeof player === 'number') {\n return player;\n }\n return null;\n}\n\n/**\n * Returns a shared object, which is automatically cleaned up when the component is unmounted.\n */\nfunction useReleasingSharedObject(\n factory: () => T,\n dependencies: DependencyList\n): T {\n const objectRef = useRef(null);\n const isFastRefresh = useRef(false);\n const previousDependencies = useRef(dependencies);\n\n if (objectRef.current == null) {\n objectRef.current = factory();\n }\n\n const object = useMemo(() => {\n let newObject = objectRef.current;\n const dependenciesAreEqual =\n previousDependencies.current?.length === dependencies.length &&\n dependencies.every((value, index) => value === previousDependencies.current[index]);\n\n // If the dependencies have changed, release the previous object and create a new one, otherwise this has been called\n // because of a fast refresh, and we don't want to release the object.\n if (!newObject || !dependenciesAreEqual) {\n objectRef.current?.release();\n newObject = factory();\n objectRef.current = newObject;\n previousDependencies.current = dependencies;\n } else {\n isFastRefresh.current = true;\n }\n return newObject;\n }, dependencies);\n\n useEffect(() => {\n isFastRefresh.current = false;\n\n return () => {\n // This will be called on every fast refresh and on unmount, but we only want to release the object on unmount.\n if (!isFastRefresh.current && objectRef.current) {\n objectRef.current.release();\n }\n };\n }, []);\n\n return object;\n}\n"]} \ No newline at end of file +{"version":3,"file":"VideoView.js","sourceRoot":"","sources":["../src/VideoView.tsx"],"names":[],"mappings":"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","sourcesContent":["import { SharedObject } from 'expo-modules-core';\nimport {\n ReactNode,\n PureComponent,\n DependencyList,\n createRef,\n useRef,\n useMemo,\n useEffect,\n} from 'react';\n\nimport NativeVideoModule from './NativeVideoModule';\nimport NativeVideoView from './NativeVideoView';\nimport { VideoPlayer, VideoSource, VideoViewProps } from './VideoView.types';\n\nexport function useVideoPlayer(\n source: VideoSource,\n setup?: (player: VideoPlayer) => void\n): VideoPlayer {\n const parsedSource = typeof source === 'string' ? { uri: source } : source;\n\n return useReleasingSharedObject(() => {\n const player = new NativeVideoModule.VideoPlayer(parsedSource);\n setup?.(player);\n return player;\n }, [JSON.stringify(parsedSource)]);\n}\n\n/**\n * Returns whether the current device supports Picture in Picture (PiP) mode.\n * @returns A `boolean` which is `true` if the device supports PiP mode, and `false` otherwise.\n * @platform android\n * @platform ios\n */\nexport function isPictureInPictureSupported(): Promise {\n return NativeVideoModule.isPictureInPictureSupported();\n}\n\nexport class VideoView extends PureComponent {\n nativeRef = createRef();\n\n replace(source: VideoSource) {\n if (typeof source === 'string') {\n this.nativeRef.current?.replace({ uri: source });\n return;\n }\n this.nativeRef.current?.replace(source);\n }\n\n enterFullscreen() {\n this.nativeRef.current?.enterFullscreen();\n }\n\n exitFullscreen() {\n this.nativeRef.current?.exitFullscreen();\n }\n\n /**\n * Enters Picture in Picture (PiP) mode. Throws an exception if the device does not support PiP.\n * > **Note:** Only one player can be in Picture in Picture (PiP) mode at a time.\n * @platform android\n * @platform ios 14+\n */\n startPictureInPicture() {\n return this.nativeRef.current?.startPictureInPicture();\n }\n\n /**\n * Exits Picture in Picture (PiP) mode.\n * @platform android\n * @platform ios 14+\n */\n stopPictureInPicture() {\n return this.nativeRef.current?.stopPictureInPicture();\n }\n\n render(): ReactNode {\n const { player, ...props } = this.props;\n const playerId = getPlayerId(player);\n\n return ;\n }\n}\n\n// Temporary solution to pass the shared object ID instead of the player object.\n// We can't really pass it as an object in the old architecture.\n// Technically we can in the new architecture, but it's not possible yet.\nfunction getPlayerId(player: number | VideoPlayer): number | null {\n if (player instanceof NativeVideoModule.VideoPlayer) {\n // @ts-expect-error\n return player.__expo_shared_object_id__;\n }\n if (typeof player === 'number') {\n return player;\n }\n return null;\n}\n\n/**\n * Returns a shared object, which is automatically cleaned up when the component is unmounted.\n */\nfunction useReleasingSharedObject(\n factory: () => T,\n dependencies: DependencyList\n): T {\n const objectRef = useRef(null);\n const isFastRefresh = useRef(false);\n const previousDependencies = useRef(dependencies);\n\n if (objectRef.current == null) {\n objectRef.current = factory();\n }\n\n const object = useMemo(() => {\n let newObject = objectRef.current;\n const dependenciesAreEqual =\n previousDependencies.current?.length === dependencies.length &&\n dependencies.every((value, index) => value === previousDependencies.current[index]);\n\n // If the dependencies have changed, release the previous object and create a new one, otherwise this has been called\n // because of a fast refresh, and we don't want to release the object.\n if (!newObject || !dependenciesAreEqual) {\n objectRef.current?.release();\n newObject = factory();\n objectRef.current = newObject;\n previousDependencies.current = dependencies;\n } else {\n isFastRefresh.current = true;\n }\n return newObject;\n }, dependencies);\n\n useEffect(() => {\n isFastRefresh.current = false;\n\n return () => {\n // This will be called on every fast refresh and on unmount, but we only want to release the object on unmount.\n if (!isFastRefresh.current && objectRef.current) {\n objectRef.current.release();\n }\n };\n }, []);\n\n return object;\n}\n"]} \ No newline at end of file diff --git a/packages/expo-video/build/VideoView.types.d.ts b/packages/expo-video/build/VideoView.types.d.ts index e599d341c7ca7..7d47366c63e96 100644 --- a/packages/expo-video/build/VideoView.types.d.ts +++ b/packages/expo-video/build/VideoView.types.d.ts @@ -3,7 +3,7 @@ import { ViewProps } from 'react-native'; /** * A class that represents an instance of the video player. */ -export declare class VideoPlayer extends SharedObject { +export declare class VideoPlayer extends SharedObject { /** * Boolean value whether the player is currently playing. * > This property is get-only, use `play` and `pause` methods to control the playback. @@ -43,6 +43,11 @@ export declare class VideoPlayer extends SharedObject { * @default 1.0 */ playbackRate: number; + /** + * Indicates the current status of the player. + * > This property is get-only + */ + status: PlayerStatus; /** * Determines whether the player should continue playing after the app enters the background. * @default false @@ -73,11 +78,19 @@ export declare class VideoPlayer extends SharedObject { } /** * Describes how a video should be scaled to fit in a container. - * 'contain': The video maintains its aspect ratio and fits inside the container, with possible letterboxing/pillarboxing. - * 'cover': The video maintains its aspect ratio and covers the entire container, potentially cropping some portions. - * 'fill': The video stretches/squeezes to completely fill the container, potentially causing distortion. + * - `contain`: The video maintains its aspect ratio and fits inside the container, with possible letterboxing/pillarboxing. + * - `cover`: The video maintains its aspect ratio and covers the entire container, potentially cropping some portions. + * - `fill`: The video stretches/squeezes to completely fill the container, potentially causing distortion. */ type VideoContentFit = 'contain' | 'cover' | 'fill'; +/** + * Describes the current status of the player. + * - `idle`: The player is not playing or loading any videos. + * - `loading`: The player is loading video data from the provided source + * - `readyToPlay`: The player has loaded enough data to start playing or to continue playback. + * - `error`: The player has encountered an error while loading or playing the video. + */ +export type PlayerStatus = 'idle' | 'loading' | 'readyToPlay' | 'error'; export interface VideoViewProps extends ViewProps { /** * A player instance – use `useVideoPlayer()` to create one. @@ -87,40 +100,40 @@ export interface VideoViewProps extends ViewProps { * Determines whether native controls should be displayed or not. * @default true */ - nativeControls: boolean | undefined; + nativeControls?: boolean; /** * Describes how the video should be scaled to fit in the container. * Options are 'contain', 'cover', and 'fill'. * @default 'contain' */ - contentFit: VideoContentFit | undefined; + contentFit?: VideoContentFit; /** * Determines whether fullscreen mode is allowed or not. * @default true */ - allowsFullscreen: boolean | undefined; + allowsFullscreen?: boolean; /** * Determines whether the timecodes should be displayed or not. * @default true * @platform ios */ - showsTimecodes: boolean | undefined; + showsTimecodes?: boolean; /** * Determines whether the player allows the user to skip media content. * @default false * @platform android * @platform ios */ - requiresLinearPlayback: boolean | undefined; + requiresLinearPlayback?: boolean; /** * Determines the position offset of the video inside the container. * @default { dx: 0, dy: 0 } * @platform ios */ - contentPosition: { + contentPosition?: { dx?: number; dy?: number; - } | undefined; + }; /** * A callback to call after the video player enters Picture in Picture (PiP) mode. * @platform android @@ -190,5 +203,47 @@ export type VideoSource = string | { uri: string; drm?: DRMOptions; } | null; +/** + * Handlers for events which can be emitted by the player. + */ +export type VideoPlayerEvents = { + /** + * Handler for an event emitted when the status of the player changes. + */ + statusChange: (newStatus: PlayerStatus, oldStatus: PlayerStatus, error: PlayerError) => void; + /** + * Handler for an event emitted when the player starts or stops playback. + */ + playingChange: (newIsPlaying: boolean, oldIsPlaying: boolean) => void; + /** + * Handler for an event emitted when the `playbackRate` property of the player changes. + */ + playbackRateChange: (newPlaybackRate: number, oldPlaybackRate: number) => void; + /** + * Handler for an event emitted when the `volume` property of the player changes. + */ + volumeChange: (newVolume: VolumeEvent, oldVolume: VolumeEvent) => void; + /** + * Handler for an event emitted when the player plays to the end of the current source. + */ + playToEnd: () => void; + /** + * Handler for an event emitted when the current media source of the player changes. + */ + sourceChange: (newSource: VideoSource, previousSource: VideoSource) => void; +}; +/** + * Contains information about any errors that the player encountered during the playback + */ +type PlayerError = { + message: string; +}; +/** + * Contains information about the current volume and whether the player is muted. + */ +type VolumeEvent = { + volume: number; + isMuted: boolean; +}; export {}; //# sourceMappingURL=VideoView.types.d.ts.map \ No newline at end of file diff --git a/packages/expo-video/build/VideoView.types.d.ts.map b/packages/expo-video/build/VideoView.types.d.ts.map index 65de877c800ad..f67846c876dc5 100644 --- a/packages/expo-video/build/VideoView.types.d.ts.map +++ b/packages/expo-video/build/VideoView.types.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"VideoView.types.d.ts","sourceRoot":"","sources":["../src/VideoView.types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzC;;GAEG;AACH,MAAM,CAAC,OAAO,OAAO,WAAY,SAAQ,YAAY;IACnD;;;OAGG;IACH,OAAO,EAAE,OAAO,CAAC;IAEjB;;;OAGG;IACH,IAAI,EAAE,OAAO,CAAC;IAEd;;;OAGG;IACH,KAAK,EAAE,OAAO,CAAC;IAEf;;OAEG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB;;;;;OAKG;IACH,MAAM,EAAE,MAAM,CAAC;IAEf;;;;;;OAMG;IACH,cAAc,EAAE,OAAO,CAAC;IAExB;;;OAGG;IACH,YAAY,EAAE,MAAM,CAAC;IAErB;;;;;OAKG;IACH,uBAAuB,EAAE,OAAO,CAAC;IAEjC;;OAEG;IACH,IAAI,IAAI,IAAI;IAEZ;;OAEG;IACH,KAAK,IAAI,IAAI;IAEb;;OAEG;IACH,OAAO,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI;IAElC;;OAEG;IACH,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAE7B;;OAEG;IACH,MAAM,IAAI,IAAI;CACf;AAED;;;;;GAKG;AACH,KAAK,eAAe,GAAG,SAAS,GAAG,OAAO,GAAG,MAAM,CAAC;AAEpD,MAAM,WAAW,cAAe,SAAQ,SAAS;IAC/C;;OAEG;IACH,MAAM,EAAE,WAAW,CAAC;IAEpB;;;OAGG;IACH,cAAc,EAAE,OAAO,GAAG,SAAS,CAAC;IAEpC;;;;OAIG;IACH,UAAU,EAAE,eAAe,GAAG,SAAS,CAAC;IAExC;;;OAGG;IACH,gBAAgB,EAAE,OAAO,GAAG,SAAS,CAAC;IAEtC;;;;OAIG;IACH,cAAc,EAAE,OAAO,GAAG,SAAS,CAAC;IAEpC;;;;;OAKG;IACH,sBAAsB,EAAE,OAAO,GAAG,SAAS,CAAC;IAE5C;;;;OAIG;IACH,eAAe,EAAE;QAAE,EAAE,CAAC,EAAE,MAAM,CAAC;QAAC,EAAE,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAC;IAE1D;;;;OAIG;IACH,uBAAuB,CAAC,EAAE,MAAM,IAAI,CAAC;IAErC;;;;OAIG;IACH,sBAAsB,CAAC,EAAE,MAAM,IAAI,CAAC;IAEpC;;;;OAIG;IACH,sBAAsB,CAAC,EAAE,OAAO,CAAC;IAEjC;;;;;;OAMG;IACH,mCAAmC,CAAC,EAAE,OAAO,CAAC;CAC/C;AAED;;KAEK;AACL,KAAK,OAAO,GAAG,UAAU,GAAG,UAAU,GAAG,WAAW,GAAG,UAAU,CAAC;AAElE;;GAEG;AACH,KAAK,UAAU,GAAG;IAChB;;OAEG;IACH,IAAI,EAAE,OAAO,CAAC;IAEd;;OAEG;IACH,aAAa,EAAE,MAAM,CAAC;IAEtB;;OAEG;IACH,OAAO,CAAC,EAAE;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;IAEpC;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,UAAU,CAAA;CAAE,GAAG,IAAI,CAAC"} \ No newline at end of file +{"version":3,"file":"VideoView.types.d.ts","sourceRoot":"","sources":["../src/VideoView.types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzC;;GAEG;AACH,MAAM,CAAC,OAAO,OAAO,WAAY,SAAQ,YAAY,CAAC,iBAAiB,CAAC;IACtE;;;OAGG;IACH,OAAO,EAAE,OAAO,CAAC;IAEjB;;;OAGG;IACH,IAAI,EAAE,OAAO,CAAC;IAEd;;;OAGG;IACH,KAAK,EAAE,OAAO,CAAC;IAEf;;OAEG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB;;;;;OAKG;IACH,MAAM,EAAE,MAAM,CAAC;IAEf;;;;;;OAMG;IACH,cAAc,EAAE,OAAO,CAAC;IAExB;;;OAGG;IACH,YAAY,EAAE,MAAM,CAAC;IAErB;;;OAGG;IACH,MAAM,EAAE,YAAY,CAAC;IAErB;;;;;OAKG;IACH,uBAAuB,EAAE,OAAO,CAAC;IAEjC;;OAEG;IACH,IAAI,IAAI,IAAI;IAEZ;;OAEG;IACH,KAAK,IAAI,IAAI;IAEb;;OAEG;IACH,OAAO,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI;IAElC;;OAEG;IACH,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAE7B;;OAEG;IACH,MAAM,IAAI,IAAI;CACf;AAED;;;;;GAKG;AACH,KAAK,eAAe,GAAG,SAAS,GAAG,OAAO,GAAG,MAAM,CAAC;AAEpD;;;;;;GAMG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,SAAS,GAAG,aAAa,GAAG,OAAO,CAAC;AAExE,MAAM,WAAW,cAAe,SAAQ,SAAS;IAC/C;;OAEG;IACH,MAAM,EAAE,WAAW,CAAC;IAEpB;;;OAGG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IAEzB;;;;OAIG;IACH,UAAU,CAAC,EAAE,eAAe,CAAC;IAE7B;;;OAGG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAE3B;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IAEzB;;;;;OAKG;IACH,sBAAsB,CAAC,EAAE,OAAO,CAAC;IAEjC;;;;OAIG;IACH,eAAe,CAAC,EAAE;QAAE,EAAE,CAAC,EAAE,MAAM,CAAC;QAAC,EAAE,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAE/C;;;;OAIG;IACH,uBAAuB,CAAC,EAAE,MAAM,IAAI,CAAC;IAErC;;;;OAIG;IACH,sBAAsB,CAAC,EAAE,MAAM,IAAI,CAAC;IAEpC;;;;OAIG;IACH,sBAAsB,CAAC,EAAE,OAAO,CAAC;IAEjC;;;;;;OAMG;IACH,mCAAmC,CAAC,EAAE,OAAO,CAAC;CAC/C;AAED;;KAEK;AACL,KAAK,OAAO,GAAG,UAAU,GAAG,UAAU,GAAG,WAAW,GAAG,UAAU,CAAC;AAElE;;GAEG;AACH,KAAK,UAAU,GAAG;IAChB;;OAEG;IACH,IAAI,EAAE,OAAO,CAAC;IAEd;;OAEG;IACH,aAAa,EAAE,MAAM,CAAC;IAEtB;;OAEG;IACH,OAAO,CAAC,EAAE;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;IAEpC;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,UAAU,CAAA;CAAE,GAAG,IAAI,CAAC;AAE5E;;GAEG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC9B;;OAEG;IACH,YAAY,EAAE,CAAC,SAAS,EAAE,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,KAAK,EAAE,WAAW,KAAK,IAAI,CAAC;IAC7F;;OAEG;IACH,aAAa,EAAE,CAAC,YAAY,EAAE,OAAO,EAAE,YAAY,EAAE,OAAO,KAAK,IAAI,CAAC;IACtE;;OAEG;IACH,kBAAkB,EAAE,CAAC,eAAe,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/E;;OAEG;IACH,YAAY,EAAE,CAAC,SAAS,EAAE,WAAW,EAAE,SAAS,EAAE,WAAW,KAAK,IAAI,CAAC;IACvE;;OAEG;IACH,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB;;OAEG;IACH,YAAY,EAAE,CAAC,SAAS,EAAE,WAAW,EAAE,cAAc,EAAE,WAAW,KAAK,IAAI,CAAC;CAC7E,CAAC;AAEF;;GAEG;AACH,KAAK,WAAW,GAAG;IACjB,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF;;GAEG;AACH,KAAK,WAAW,GAAG;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;CAClB,CAAC"} \ No newline at end of file diff --git a/packages/expo-video/build/VideoView.types.js.map b/packages/expo-video/build/VideoView.types.js.map index e29713af0b684..24305ed664850 100644 --- a/packages/expo-video/build/VideoView.types.js.map +++ b/packages/expo-video/build/VideoView.types.js.map @@ -1 +1 @@ -{"version":3,"file":"VideoView.types.js","sourceRoot":"","sources":["../src/VideoView.types.ts"],"names":[],"mappings":"","sourcesContent":["import type { SharedObject } from 'expo-modules-core';\nimport { ViewProps } from 'react-native';\n\n/**\n * A class that represents an instance of the video player.\n */\nexport declare class VideoPlayer extends SharedObject {\n /**\n * Boolean value whether the player is currently playing.\n * > This property is get-only, use `play` and `pause` methods to control the playback.\n */\n playing: boolean;\n\n /**\n * Determines whether the player should automatically replay after reaching the end of the video.\n * @default false\n */\n loop: boolean;\n\n /**\n * Boolean value whether the player is currently muted.\n * @default false\n */\n muted: boolean;\n\n /**\n * Integer value representing the current position in seconds.\n */\n currentTime: number;\n\n /**\n * Float value between 0 and 1 representing the current volume.\n * Muting the player doesn't affect the volume. In other words, when the player is muted, the volume is the same as\n * when unmuted. Similarly, setting the volume doesn't unmute the player.\n * @default 1.0\n */\n volume: number;\n\n /**\n * Boolean value indicating if the player should correct audio pitch when the playback speed changes.\n * > On web, changing this property is not supported, the player will always correct the pitch.\n * @default true\n * @platform android\n * @platform ios\n */\n preservesPitch: boolean;\n\n /**\n * Float value between 0 and 16 indicating the current playback speed of the player.\n * @default 1.0\n */\n playbackRate: number;\n\n /**\n * Determines whether the player should continue playing after the app enters the background.\n * @default false\n * @platform ios\n * @platform android\n */\n staysActiveInBackground: boolean;\n\n /**\n * Resumes the player.\n */\n play(): void;\n\n /**\n * Pauses the player.\n */\n pause(): void;\n\n /**\n * Replaces the current source with a new one.\n */\n replace(source: VideoSource): void;\n\n /**\n * Seeks the playback by the given number of seconds.\n */\n seekBy(seconds: number): void;\n\n /**\n * Seeks the playback to the beginning.\n */\n replay(): void;\n}\n\n/**\n * Describes how a video should be scaled to fit in a container.\n * 'contain': The video maintains its aspect ratio and fits inside the container, with possible letterboxing/pillarboxing.\n * 'cover': The video maintains its aspect ratio and covers the entire container, potentially cropping some portions.\n * 'fill': The video stretches/squeezes to completely fill the container, potentially causing distortion.\n */\ntype VideoContentFit = 'contain' | 'cover' | 'fill';\n\nexport interface VideoViewProps extends ViewProps {\n /**\n * A player instance – use `useVideoPlayer()` to create one.\n */\n player: VideoPlayer;\n\n /**\n * Determines whether native controls should be displayed or not.\n * @default true\n */\n nativeControls: boolean | undefined;\n\n /**\n * Describes how the video should be scaled to fit in the container.\n * Options are 'contain', 'cover', and 'fill'.\n * @default 'contain'\n */\n contentFit: VideoContentFit | undefined;\n\n /**\n * Determines whether fullscreen mode is allowed or not.\n * @default true\n */\n allowsFullscreen: boolean | undefined;\n\n /**\n * Determines whether the timecodes should be displayed or not.\n * @default true\n * @platform ios\n */\n showsTimecodes: boolean | undefined;\n\n /**\n * Determines whether the player allows the user to skip media content.\n * @default false\n * @platform android\n * @platform ios\n */\n requiresLinearPlayback: boolean | undefined;\n\n /**\n * Determines the position offset of the video inside the container.\n * @default { dx: 0, dy: 0 }\n * @platform ios\n */\n contentPosition: { dx?: number; dy?: number } | undefined;\n\n /**\n * A callback to call after the video player enters Picture in Picture (PiP) mode.\n * @platform android\n * @platform ios 14+\n */\n onPictureInPictureStart?: () => void;\n\n /**\n * A callback to call after the video player exits Picture in Picture (PiP) mode.\n * @platform android\n * @platform ios 14+\n */\n onPictureInPictureStop?: () => void;\n\n /**\n * Determines whether the player allows Picture in Picture (PiP) mode.\n * @default false\n * @platform ios 14+\n */\n allowsPictureInPicture?: boolean;\n\n /**\n * Determines whether the player should start Picture in Picture (PiP) automatically when the app is in the background.\n * > **Note:** Only one player can be in Picture in Picture (PiP) mode at a time.\n * @default false\n * @platform android 12+\n * @platform ios 14.2+\n */\n startsPictureInPictureAutomatically?: boolean;\n}\n\n/**\n * Specifies which type of DRM to use. Android supports Widevine, PlayReady and ClearKey, iOS supports FairPlay.\n * */\ntype DRMType = 'clearkey' | 'fairplay' | 'playready' | 'widevine';\n\n/**\n * Specifies DRM options which will be used by the player while loading the video.\n */\ntype DRMOptions = {\n /**\n * Determines which type of DRM to use.\n */\n type: DRMType;\n\n /**\n * Determines the license server URL.\n */\n licenseServer: string;\n\n /**\n * Determines headers sent to the license server on license requests.\n */\n headers?: { [key: string]: string };\n\n /**\n * Specifies whether the DRM is a multi-key DRM.\n * @platform android\n */\n multiKey?: boolean;\n\n /**\n * Specifies the content ID of the stream.\n * @platform ios\n */\n contentId?: string;\n\n /**\n * Specifies the certificate URL for the FairPlay DRM.\n * @platform ios\n */\n certificateUrl?: string;\n};\n\nexport type VideoSource = string | { uri: string; drm?: DRMOptions } | null;\n"]} \ No newline at end of file +{"version":3,"file":"VideoView.types.js","sourceRoot":"","sources":["../src/VideoView.types.ts"],"names":[],"mappings":"","sourcesContent":["import type { SharedObject } from 'expo-modules-core';\nimport { ViewProps } from 'react-native';\n\n/**\n * A class that represents an instance of the video player.\n */\nexport declare class VideoPlayer extends SharedObject {\n /**\n * Boolean value whether the player is currently playing.\n * > This property is get-only, use `play` and `pause` methods to control the playback.\n */\n playing: boolean;\n\n /**\n * Determines whether the player should automatically replay after reaching the end of the video.\n * @default false\n */\n loop: boolean;\n\n /**\n * Boolean value whether the player is currently muted.\n * @default false\n */\n muted: boolean;\n\n /**\n * Integer value representing the current position in seconds.\n */\n currentTime: number;\n\n /**\n * Float value between 0 and 1 representing the current volume.\n * Muting the player doesn't affect the volume. In other words, when the player is muted, the volume is the same as\n * when unmuted. Similarly, setting the volume doesn't unmute the player.\n * @default 1.0\n */\n volume: number;\n\n /**\n * Boolean value indicating if the player should correct audio pitch when the playback speed changes.\n * > On web, changing this property is not supported, the player will always correct the pitch.\n * @default true\n * @platform android\n * @platform ios\n */\n preservesPitch: boolean;\n\n /**\n * Float value between 0 and 16 indicating the current playback speed of the player.\n * @default 1.0\n */\n playbackRate: number;\n\n /**\n * Indicates the current status of the player.\n * > This property is get-only\n */\n status: PlayerStatus;\n\n /**\n * Determines whether the player should continue playing after the app enters the background.\n * @default false\n * @platform ios\n * @platform android\n */\n staysActiveInBackground: boolean;\n\n /**\n * Resumes the player.\n */\n play(): void;\n\n /**\n * Pauses the player.\n */\n pause(): void;\n\n /**\n * Replaces the current source with a new one.\n */\n replace(source: VideoSource): void;\n\n /**\n * Seeks the playback by the given number of seconds.\n */\n seekBy(seconds: number): void;\n\n /**\n * Seeks the playback to the beginning.\n */\n replay(): void;\n}\n\n/**\n * Describes how a video should be scaled to fit in a container.\n * - `contain`: The video maintains its aspect ratio and fits inside the container, with possible letterboxing/pillarboxing.\n * - `cover`: The video maintains its aspect ratio and covers the entire container, potentially cropping some portions.\n * - `fill`: The video stretches/squeezes to completely fill the container, potentially causing distortion.\n */\ntype VideoContentFit = 'contain' | 'cover' | 'fill';\n\n/**\n * Describes the current status of the player.\n * - `idle`: The player is not playing or loading any videos.\n * - `loading`: The player is loading video data from the provided source\n * - `readyToPlay`: The player has loaded enough data to start playing or to continue playback.\n * - `error`: The player has encountered an error while loading or playing the video.\n */\nexport type PlayerStatus = 'idle' | 'loading' | 'readyToPlay' | 'error';\n\nexport interface VideoViewProps extends ViewProps {\n /**\n * A player instance – use `useVideoPlayer()` to create one.\n */\n player: VideoPlayer;\n\n /**\n * Determines whether native controls should be displayed or not.\n * @default true\n */\n nativeControls?: boolean;\n\n /**\n * Describes how the video should be scaled to fit in the container.\n * Options are 'contain', 'cover', and 'fill'.\n * @default 'contain'\n */\n contentFit?: VideoContentFit;\n\n /**\n * Determines whether fullscreen mode is allowed or not.\n * @default true\n */\n allowsFullscreen?: boolean;\n\n /**\n * Determines whether the timecodes should be displayed or not.\n * @default true\n * @platform ios\n */\n showsTimecodes?: boolean;\n\n /**\n * Determines whether the player allows the user to skip media content.\n * @default false\n * @platform android\n * @platform ios\n */\n requiresLinearPlayback?: boolean;\n\n /**\n * Determines the position offset of the video inside the container.\n * @default { dx: 0, dy: 0 }\n * @platform ios\n */\n contentPosition?: { dx?: number; dy?: number };\n\n /**\n * A callback to call after the video player enters Picture in Picture (PiP) mode.\n * @platform android\n * @platform ios 14+\n */\n onPictureInPictureStart?: () => void;\n\n /**\n * A callback to call after the video player exits Picture in Picture (PiP) mode.\n * @platform android\n * @platform ios 14+\n */\n onPictureInPictureStop?: () => void;\n\n /**\n * Determines whether the player allows Picture in Picture (PiP) mode.\n * @default false\n * @platform ios 14+\n */\n allowsPictureInPicture?: boolean;\n\n /**\n * Determines whether the player should start Picture in Picture (PiP) automatically when the app is in the background.\n * > **Note:** Only one player can be in Picture in Picture (PiP) mode at a time.\n * @default false\n * @platform android 12+\n * @platform ios 14.2+\n */\n startsPictureInPictureAutomatically?: boolean;\n}\n\n/**\n * Specifies which type of DRM to use. Android supports Widevine, PlayReady and ClearKey, iOS supports FairPlay.\n * */\ntype DRMType = 'clearkey' | 'fairplay' | 'playready' | 'widevine';\n\n/**\n * Specifies DRM options which will be used by the player while loading the video.\n */\ntype DRMOptions = {\n /**\n * Determines which type of DRM to use.\n */\n type: DRMType;\n\n /**\n * Determines the license server URL.\n */\n licenseServer: string;\n\n /**\n * Determines headers sent to the license server on license requests.\n */\n headers?: { [key: string]: string };\n\n /**\n * Specifies whether the DRM is a multi-key DRM.\n * @platform android\n */\n multiKey?: boolean;\n\n /**\n * Specifies the content ID of the stream.\n * @platform ios\n */\n contentId?: string;\n\n /**\n * Specifies the certificate URL for the FairPlay DRM.\n * @platform ios\n */\n certificateUrl?: string;\n};\n\nexport type VideoSource = string | { uri: string; drm?: DRMOptions } | null;\n\n/**\n * Handlers for events which can be emitted by the player.\n */\nexport type VideoPlayerEvents = {\n /**\n * Handler for an event emitted when the status of the player changes.\n */\n statusChange: (newStatus: PlayerStatus, oldStatus: PlayerStatus, error: PlayerError) => void;\n /**\n * Handler for an event emitted when the player starts or stops playback.\n */\n playingChange: (newIsPlaying: boolean, oldIsPlaying: boolean) => void;\n /**\n * Handler for an event emitted when the `playbackRate` property of the player changes.\n */\n playbackRateChange: (newPlaybackRate: number, oldPlaybackRate: number) => void;\n /**\n * Handler for an event emitted when the `volume` property of the player changes.\n */\n volumeChange: (newVolume: VolumeEvent, oldVolume: VolumeEvent) => void;\n /**\n * Handler for an event emitted when the player plays to the end of the current source.\n */\n playToEnd: () => void;\n /**\n * Handler for an event emitted when the current media source of the player changes.\n */\n sourceChange: (newSource: VideoSource, previousSource: VideoSource) => void;\n};\n\n/**\n * Contains information about any errors that the player encountered during the playback\n */\ntype PlayerError = {\n message: string;\n};\n\n/**\n * Contains information about the current volume and whether the player is muted.\n */\ntype VolumeEvent = {\n volume: number;\n isMuted: boolean;\n};\n"]} \ No newline at end of file diff --git a/packages/expo-video/build/VideoView.web.d.ts b/packages/expo-video/build/VideoView.web.d.ts index c174858281a0f..2719cd29649c2 100644 --- a/packages/expo-video/build/VideoView.web.d.ts +++ b/packages/expo-video/build/VideoView.web.d.ts @@ -1,8 +1,8 @@ import React from 'react'; -import { VideoPlayer, VideoViewProps } from './VideoView.types'; +import { PlayerStatus, VideoPlayer, VideoSource, VideoViewProps } from './VideoView.types'; declare class VideoPlayerWeb implements VideoPlayer { - constructor(source?: string | null); - src: string | null; + constructor(source: VideoSource); + src: VideoSource; _mountedVideos: Set; _audioNodes: Set; playing: boolean; @@ -11,6 +11,7 @@ declare class VideoPlayerWeb implements VideoPlayer { _loop: boolean; _playbackRate: number; _preservesPitch: boolean; + _status: PlayerStatus; staysActiveInBackground: boolean; set muted(value: boolean); get muted(): boolean; @@ -24,11 +25,12 @@ declare class VideoPlayerWeb implements VideoPlayer { set currentTime(value: number); get preservesPitch(): boolean; set preservesPitch(value: boolean); + get status(): PlayerStatus; mountVideoView(video: HTMLVideoElement): void; unmountVideoView(video: HTMLVideoElement): void; play(): void; pause(): void; - replace(source: string): void; + replace(source: VideoSource): void; seekBy(seconds: number): void; replay(): void; _synchronizeWithFirstVideo(video: HTMLVideoElement): void; @@ -39,7 +41,7 @@ declare class VideoPlayerWeb implements VideoPlayer { removeAllListeners(eventName: never): void; emit(eventName: EventName, ...args: Parameters[EventName]>): void; } -export declare function useVideoPlayer(source?: string | null): VideoPlayer; +export declare function useVideoPlayer(source: VideoSource, setup?: (player: VideoPlayer) => void): VideoPlayer; export declare const VideoView: React.ForwardRefExoticComponent<{ player?: VideoPlayerWeb | undefined; } & VideoViewProps & React.RefAttributes>; diff --git a/packages/expo-video/build/VideoView.web.d.ts.map b/packages/expo-video/build/VideoView.web.d.ts.map index 691d17d8efa8d..2a28e25f2f7dc 100644 --- a/packages/expo-video/build/VideoView.web.d.ts.map +++ b/packages/expo-video/build/VideoView.web.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"VideoView.web.d.ts","sourceRoot":"","sources":["../src/VideoView.web.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA6D,MAAM,OAAO,CAAC;AAGlF,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAiBhE,cAAM,cAAe,YAAW,WAAW;gBAC7B,MAAM,GAAE,MAAM,GAAG,IAAW;IAIxC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAQ;IAC1B,cAAc,EAAE,GAAG,CAAC,gBAAgB,CAAC,CAAa;IAClD,WAAW,EAAE,GAAG,CAAC,2BAA2B,CAAC,CAAa;IAC1D,OAAO,EAAE,OAAO,CAAS;IACzB,MAAM,EAAE,OAAO,CAAS;IACxB,OAAO,EAAE,MAAM,CAAK;IACpB,KAAK,EAAE,OAAO,CAAS;IACvB,aAAa,EAAE,MAAM,CAAO;IAC5B,eAAe,EAAE,OAAO,CAAQ;IAChC,uBAAuB,EAAE,OAAO,CAAS;IAEzC,IAAI,KAAK,CAAC,KAAK,EAAE,OAAO,EAKvB;IACD,IAAI,KAAK,IAAI,OAAO,CAEnB;IAED,IAAI,YAAY,CAAC,KAAK,EAAE,MAAM,EAI7B;IAED,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED,IAAI,MAAM,CAAC,KAAK,EAAE,MAAM,EAKvB;IAED,IAAI,MAAM,IAAI,MAAM,CAKnB;IAED,IAAI,IAAI,CAAC,KAAK,EAAE,OAAO,EAKtB;IAED,IAAI,IAAI,IAAI,OAAO,CAElB;IAED,IAAI,WAAW,IAAI,MAAM,CAGxB;IAED,IAAI,WAAW,CAAC,KAAK,EAAE,MAAM,EAI5B;IAED,IAAI,cAAc,IAAI,OAAO,CAE5B;IACD,IAAI,cAAc,CAAC,KAAK,EAAE,OAAO,EAKhC;IAED,cAAc,CAAC,KAAK,EAAE,gBAAgB;IAMtC,gBAAgB,CAAC,KAAK,EAAE,gBAAgB;IAgBxC,IAAI,IAAI,IAAI;IAMZ,KAAK,IAAI,IAAI;IAMb,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAS7B,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAK7B,MAAM,IAAI,IAAI;IAQd,0BAA0B,CAAC,KAAK,EAAE,gBAAgB,GAAG,IAAI;IASzD,aAAa,CAAC,KAAK,EAAE,gBAAgB,GAAG,IAAI;IAwD5C,OAAO,IAAI,IAAI;IAGf,WAAW,CAAC,SAAS,SAAS,KAAK,EACjC,SAAS,EAAE,SAAS,EACpB,QAAQ,EAAE,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,SAAS,CAAC,GACxC,IAAI;IAGP,cAAc,CAAC,SAAS,SAAS,KAAK,EACpC,SAAS,EAAE,SAAS,EACpB,QAAQ,EAAE,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,SAAS,CAAC,GACxC,IAAI;IAGP,kBAAkB,CAAC,SAAS,EAAE,KAAK,GAAG,IAAI;IAG1C,IAAI,CAAC,SAAS,SAAS,KAAK,EAC1B,SAAS,EAAE,SAAS,EACpB,GAAG,IAAI,EAAE,UAAU,CAAC,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,SAAS,CAAC,CAAC,GACnD,IAAI;CAGR;AAQD,wBAAgB,cAAc,CAAC,MAAM,GAAE,MAAM,GAAG,IAAW,GAAG,WAAW,CAKxE;AAED,eAAO,MAAM,SAAS;;kDAqDpB,CAAC;AAEH,eAAe,SAAS,CAAC"} \ No newline at end of file +{"version":3,"file":"VideoView.web.d.ts","sourceRoot":"","sources":["../src/VideoView.web.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAsE,MAAM,OAAO,CAAC;AAG3F,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAe3F,cAAM,cAAe,YAAW,WAAW;gBAC7B,MAAM,EAAE,WAAW;IAI/B,GAAG,EAAE,WAAW,CAAQ;IACxB,cAAc,EAAE,GAAG,CAAC,gBAAgB,CAAC,CAAa;IAClD,WAAW,EAAE,GAAG,CAAC,2BAA2B,CAAC,CAAa;IAC1D,OAAO,EAAE,OAAO,CAAS;IACzB,MAAM,EAAE,OAAO,CAAS;IACxB,OAAO,EAAE,MAAM,CAAK;IACpB,KAAK,EAAE,OAAO,CAAS;IACvB,aAAa,EAAE,MAAM,CAAO;IAC5B,eAAe,EAAE,OAAO,CAAQ;IAChC,OAAO,EAAE,YAAY,CAAU;IAC/B,uBAAuB,EAAE,OAAO,CAAS;IAEzC,IAAI,KAAK,CAAC,KAAK,EAAE,OAAO,EAKvB;IACD,IAAI,KAAK,IAAI,OAAO,CAEnB;IAED,IAAI,YAAY,CAAC,KAAK,EAAE,MAAM,EAI7B;IAED,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED,IAAI,MAAM,CAAC,KAAK,EAAE,MAAM,EAKvB;IAED,IAAI,MAAM,IAAI,MAAM,CAKnB;IAED,IAAI,IAAI,CAAC,KAAK,EAAE,OAAO,EAKtB;IAED,IAAI,IAAI,IAAI,OAAO,CAElB;IAED,IAAI,WAAW,IAAI,MAAM,CAGxB;IAED,IAAI,WAAW,CAAC,KAAK,EAAE,MAAM,EAI5B;IAED,IAAI,cAAc,IAAI,OAAO,CAE5B;IACD,IAAI,cAAc,CAAC,KAAK,EAAE,OAAO,EAKhC;IAED,IAAI,MAAM,IAAI,YAAY,CAEzB;IAED,cAAc,CAAC,KAAK,EAAE,gBAAgB;IAMtC,gBAAgB,CAAC,KAAK,EAAE,gBAAgB;IAgBxC,IAAI,IAAI,IAAI;IAMZ,KAAK,IAAI,IAAI;IAMb,OAAO,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI;IAclC,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAK7B,MAAM,IAAI,IAAI;IAQd,0BAA0B,CAAC,KAAK,EAAE,gBAAgB,GAAG,IAAI;IASzD,aAAa,CAAC,KAAK,EAAE,gBAAgB,GAAG,IAAI;IAoE5C,OAAO,IAAI,IAAI;IAGf,WAAW,CAAC,SAAS,SAAS,KAAK,EACjC,SAAS,EAAE,SAAS,EACpB,QAAQ,EAAE,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,SAAS,CAAC,GACxC,IAAI;IAGP,cAAc,CAAC,SAAS,SAAS,KAAK,EACpC,SAAS,EAAE,SAAS,EACpB,QAAQ,EAAE,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,SAAS,CAAC,GACxC,IAAI;IAGP,kBAAkB,CAAC,SAAS,EAAE,KAAK,GAAG,IAAI;IAG1C,IAAI,CAAC,SAAS,SAAS,KAAK,EAC1B,SAAS,EAAE,SAAS,EACpB,GAAG,IAAI,EAAE,UAAU,CAAC,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,SAAS,CAAC,CAAC,GACnD,IAAI;CAGR;AAQD,wBAAgB,cAAc,CAC5B,MAAM,EAAE,WAAW,EACnB,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,GACpC,WAAW,CAQb;AASD,eAAO,MAAM,SAAS;;kDAqDpB,CAAC;AAEH,eAAe,SAAS,CAAC"} \ No newline at end of file diff --git a/packages/expo-video/build/VideoView.web.js b/packages/expo-video/build/VideoView.web.js index 58b9629dacaef..d0d5186250f13 100644 --- a/packages/expo-video/build/VideoView.web.js +++ b/packages/expo-video/build/VideoView.web.js @@ -1,4 +1,4 @@ -import React, { useEffect, useRef, forwardRef, useImperativeHandle } from 'react'; +import React, { useEffect, useRef, forwardRef, useImperativeHandle, useMemo } from 'react'; import { StyleSheet } from 'react-native'; /** * This audio context is used to mute all but one video when multiple video views are playing from one player simultaneously. @@ -14,7 +14,7 @@ else { console.warn("Couldn't create AudioContext, this might affect the audio playback when using multiple video views with the same player."); } class VideoPlayerWeb { - constructor(source = null) { + constructor(source) { this.src = source; } src = null; @@ -26,6 +26,7 @@ class VideoPlayerWeb { _loop = false; _playbackRate = 1.0; _preservesPitch = true; + _status = 'idle'; staysActiveInBackground = false; // Not supported on web. Dummy to match the interface. set muted(value) { this._mountedVideos.forEach((video) => { @@ -83,6 +84,9 @@ class VideoPlayerWeb { }); this._preservesPitch = value; } + get status() { + return this._status; + } mountVideoView(video) { this._mountedVideos.add(video); this._synchronizeWithFirstVideo(video); @@ -116,10 +120,16 @@ class VideoPlayerWeb { } replace(source) { this._mountedVideos.forEach((video) => { + const uri = getSourceUri(source); video.pause(); - video.setAttribute('src', source); - video.load(); - video.play(); + if (uri) { + video.setAttribute('src', uri); + video.load(); + video.play(); + } + else { + video.removeAttribute('src'); + } }); this.playing = true; } @@ -196,6 +206,15 @@ class VideoPlayerWeb { mountedVideo.playbackRate = video.playbackRate; }); }; + video.onerror = () => { + this._status = 'error'; + }; + video.onloadeddata = () => { + this._status = 'readyToPlay'; + }; + video.onwaiting = () => { + this._status = 'loading'; + }; } release() { console.warn('The `VideoPlayer.release` method is not supported on web'); @@ -218,11 +237,19 @@ function mapStyles(style) { // Looking through react-native-web source code they also just pass styles directly without further conversions, so it's just a cast. return flattenedStyles; } -export function useVideoPlayer(source = null) { - return React.useMemo(() => { - return new VideoPlayerWeb(source); - // should this not include source? - }, []); +export function useVideoPlayer(source, setup) { + const parsedSource = typeof source === 'string' ? { uri: source } : source; + return useMemo(() => { + const player = new VideoPlayerWeb(parsedSource); + setup?.(player); + return player; + }, [JSON.stringify(source)]); +} +function getSourceUri(source) { + if (typeof source == 'string') { + return source; + } + return source?.uri ?? null; } export const VideoView = forwardRef((props, ref) => { const videoRef = useRef(null); @@ -264,7 +291,7 @@ export const VideoView = forwardRef((props, ref) => { if (newRef) { videoRef.current = newRef; } - }} src={props.player?.src ?? ''}/>); + }} src={getSourceUri(props.player?.src) ?? ''}/>); }); export default VideoView; //# sourceMappingURL=VideoView.web.js.map \ No newline at end of file diff --git a/packages/expo-video/build/VideoView.web.js.map b/packages/expo-video/build/VideoView.web.js.map index eb2da08eeb2c1..abf4d23bfbbef 100644 --- a/packages/expo-video/build/VideoView.web.js.map +++ b/packages/expo-video/build/VideoView.web.js.map @@ -1 +1 @@ -{"version":3,"file":"VideoView.web.js","sourceRoot":"","sources":["../src/VideoView.web.tsx"],"names":[],"mappings":"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","sourcesContent":["import React, { useEffect, useRef, forwardRef, useImperativeHandle } from 'react';\nimport { StyleSheet } from 'react-native';\n\nimport { VideoPlayer, VideoViewProps } from './VideoView.types';\n\n/**\n * This audio context is used to mute all but one video when multiple video views are playing from one player simultaneously.\n * Using audio context nodes allows muting videos without displaying the mute icon in the video player.\n */\nconst audioContext = window && new window.AudioContext();\nconst zeroGainNode = audioContext && audioContext.createGain();\nif (audioContext && zeroGainNode) {\n zeroGainNode.gain.value = 0;\n zeroGainNode.connect(audioContext.destination);\n} else {\n console.warn(\n \"Couldn't create AudioContext, this might affect the audio playback when using multiple video views with the same player.\"\n );\n}\n\nclass VideoPlayerWeb implements VideoPlayer {\n constructor(source: string | null = null) {\n this.src = source;\n }\n\n src: string | null = null;\n _mountedVideos: Set = new Set();\n _audioNodes: Set = new Set();\n playing: boolean = false;\n _muted: boolean = false;\n _volume: number = 1;\n _loop: boolean = false;\n _playbackRate: number = 1.0;\n _preservesPitch: boolean = true;\n staysActiveInBackground: boolean = false; // Not supported on web. Dummy to match the interface.\n\n set muted(value: boolean) {\n this._mountedVideos.forEach((video) => {\n video.muted = value;\n });\n this._muted = value;\n }\n get muted(): boolean {\n return this._muted;\n }\n\n set playbackRate(value: number) {\n this._mountedVideos.forEach((video) => {\n video.playbackRate = value;\n });\n }\n\n get playbackRate(): number {\n return this._playbackRate;\n }\n\n set volume(value: number) {\n this._mountedVideos.forEach((video) => {\n video.volume = value;\n });\n this._volume = value;\n }\n\n get volume(): number {\n this._mountedVideos.forEach((video) => {\n this._volume = video.volume;\n });\n return this._volume;\n }\n\n set loop(value: boolean) {\n this._mountedVideos.forEach((video) => {\n video.loop = value;\n });\n this._loop = value;\n }\n\n get loop(): boolean {\n return this._loop;\n }\n\n get currentTime(): number {\n // All videos should be synchronized, so we return the position of the first video.\n return [...this._mountedVideos][0].currentTime;\n }\n\n set currentTime(value: number) {\n this._mountedVideos.forEach((video) => {\n video.currentTime = value;\n });\n }\n\n get preservesPitch(): boolean {\n return this._preservesPitch;\n }\n set preservesPitch(value: boolean) {\n this._mountedVideos.forEach((video) => {\n video.preservesPitch = value;\n });\n this._preservesPitch = value;\n }\n\n mountVideoView(video: HTMLVideoElement) {\n this._mountedVideos.add(video);\n this._synchronizeWithFirstVideo(video);\n this._addListeners(video);\n }\n\n unmountVideoView(video: HTMLVideoElement) {\n const mountedVideos = [...this._mountedVideos];\n const mediaElementSources = [...this._audioNodes];\n const videoIndex = mountedVideos.findIndex((value) => value === video);\n const videoPlayingAudio = mountedVideos[0];\n this._mountedVideos.delete(video);\n this._audioNodes.delete(mediaElementSources[videoIndex]);\n\n // If video playing audio has been removed, select a new video to be the audio player by disconnecting it from the mute node.\n if (videoPlayingAudio === video && this._audioNodes.size > 0 && audioContext) {\n const newMainAudioSource = [...this._audioNodes][0];\n newMainAudioSource.disconnect();\n newMainAudioSource.connect(audioContext.destination);\n }\n }\n\n play(): void {\n this._mountedVideos.forEach((video) => {\n video.play();\n });\n this.playing = true;\n }\n pause(): void {\n this._mountedVideos.forEach((video) => {\n video.pause();\n });\n this.playing = false;\n }\n replace(source: string): void {\n this._mountedVideos.forEach((video) => {\n video.pause();\n video.setAttribute('src', source);\n video.load();\n video.play();\n });\n this.playing = true;\n }\n seekBy(seconds: number): void {\n this._mountedVideos.forEach((video) => {\n video.currentTime += seconds;\n });\n }\n replay(): void {\n this._mountedVideos.forEach((video) => {\n video.currentTime = 0;\n video.play();\n });\n this.playing = true;\n }\n\n _synchronizeWithFirstVideo(video: HTMLVideoElement): void {\n const firstVideo = [...this._mountedVideos][0];\n if (!firstVideo) return;\n video.currentTime = firstVideo.currentTime;\n video.volume = firstVideo.volume;\n video.muted = firstVideo.muted;\n video.playbackRate = firstVideo.playbackRate;\n }\n\n _addListeners(video: HTMLVideoElement): void {\n video.onloadedmetadata = () => {\n if (!audioContext || !zeroGainNode) return;\n const source = audioContext.createMediaElementSource(video);\n this._audioNodes.add(source);\n\n // First mounted video should be connected to the audio context. All other videos have to be muted.\n if (this._audioNodes.size === 1) {\n source.connect(audioContext.destination);\n } else {\n source.connect(zeroGainNode);\n }\n };\n\n video.onplay = () => {\n this.playing = true;\n this._mountedVideos.forEach((mountedVideo) => {\n mountedVideo.play();\n });\n };\n\n video.onpause = () => {\n this.playing = false;\n this._mountedVideos.forEach((mountedVideo) => {\n mountedVideo.pause();\n });\n };\n\n video.onvolumechange = () => {\n this.volume = video.volume;\n this._muted = video.muted;\n };\n\n video.onseeking = () => {\n this._mountedVideos.forEach((mountedVideo) => {\n if (mountedVideo === video || mountedVideo.currentTime === video.currentTime) return;\n mountedVideo.currentTime = video.currentTime;\n });\n };\n\n video.onseeked = () => {\n this._mountedVideos.forEach((mountedVideo) => {\n if (mountedVideo === video || mountedVideo.currentTime === video.currentTime) return;\n mountedVideo.currentTime = video.currentTime;\n });\n };\n\n video.onratechange = () => {\n this._mountedVideos.forEach((mountedVideo) => {\n if (mountedVideo === video || mountedVideo.playbackRate === video.playbackRate) return;\n this._playbackRate = video.playbackRate;\n mountedVideo.playbackRate = video.playbackRate;\n });\n };\n }\n\n release(): void {\n console.warn('The `VideoPlayer.release` method is not supported on web');\n }\n addListener(\n eventName: EventName,\n listener: Record[EventName]\n ): void {\n console.warn('The `VideoPlayer.addListener` method is not yet supported on web');\n }\n removeListener(\n eventName: EventName,\n listener: Record[EventName]\n ): void {\n console.warn('The `VideoPlayer.removeListener` method is not yet supported on web');\n }\n removeAllListeners(eventName: never): void {\n console.warn('The `VideoPlayer.removeAllListeners` method is not yet supported on web');\n }\n emit(\n eventName: EventName,\n ...args: Parameters[EventName]>\n ): void {\n console.warn('The `VideoPlayer.emit` method is not yet supported on web');\n }\n}\n\nfunction mapStyles(style: VideoViewProps['style']): React.CSSProperties {\n const flattenedStyles = StyleSheet.flatten(style);\n // Looking through react-native-web source code they also just pass styles directly without further conversions, so it's just a cast.\n return flattenedStyles as React.CSSProperties;\n}\n\nexport function useVideoPlayer(source: string | null = null): VideoPlayer {\n return React.useMemo(() => {\n return new VideoPlayerWeb(source);\n // should this not include source?\n }, []);\n}\n\nexport const VideoView = forwardRef((props: { player?: VideoPlayerWeb } & VideoViewProps, ref) => {\n const videoRef = useRef(null);\n useImperativeHandle(ref, () => ({\n enterFullscreen: () => {\n if (!props.allowsFullscreen) {\n return;\n }\n videoRef.current?.requestFullscreen();\n },\n exitFullscreen: () => {\n document.exitFullscreen();\n },\n }));\n\n useEffect(() => {\n return () => {\n if (videoRef.current) {\n props.player?.unmountVideoView(videoRef.current);\n }\n };\n }, []);\n\n useEffect(() => {\n if (!props.player || !videoRef.current) {\n return;\n }\n props.player.mountVideoView(videoRef.current);\n return () => {\n if (videoRef.current) {\n props.player?.unmountVideoView(videoRef.current);\n }\n };\n }, [props.player]);\n\n return (\n {\n // This is called with a null value before `player.unmountVideoView` is called,\n // we can't assign null to videoRef if we want to unmount it from the player.\n if (newRef) {\n videoRef.current = newRef;\n }\n }}\n src={props.player?.src ?? ''}\n />\n );\n});\n\nexport default VideoView;\n"]} \ No newline at end of file +{"version":3,"file":"VideoView.web.js","sourceRoot":"","sources":["../src/VideoView.web.tsx"],"names":[],"mappings":"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","sourcesContent":["import React, { useEffect, useRef, forwardRef, useImperativeHandle, useMemo } from 'react';\nimport { StyleSheet } from 'react-native';\n\nimport { PlayerStatus, VideoPlayer, VideoSource, VideoViewProps } from './VideoView.types';\n/**\n * This audio context is used to mute all but one video when multiple video views are playing from one player simultaneously.\n * Using audio context nodes allows muting videos without displaying the mute icon in the video player.\n */\nconst audioContext = window && new window.AudioContext();\nconst zeroGainNode = audioContext && audioContext.createGain();\nif (audioContext && zeroGainNode) {\n zeroGainNode.gain.value = 0;\n zeroGainNode.connect(audioContext.destination);\n} else {\n console.warn(\n \"Couldn't create AudioContext, this might affect the audio playback when using multiple video views with the same player.\"\n );\n}\nclass VideoPlayerWeb implements VideoPlayer {\n constructor(source: VideoSource) {\n this.src = source;\n }\n\n src: VideoSource = null;\n _mountedVideos: Set = new Set();\n _audioNodes: Set = new Set();\n playing: boolean = false;\n _muted: boolean = false;\n _volume: number = 1;\n _loop: boolean = false;\n _playbackRate: number = 1.0;\n _preservesPitch: boolean = true;\n _status: PlayerStatus = 'idle';\n staysActiveInBackground: boolean = false; // Not supported on web. Dummy to match the interface.\n\n set muted(value: boolean) {\n this._mountedVideos.forEach((video) => {\n video.muted = value;\n });\n this._muted = value;\n }\n get muted(): boolean {\n return this._muted;\n }\n\n set playbackRate(value: number) {\n this._mountedVideos.forEach((video) => {\n video.playbackRate = value;\n });\n }\n\n get playbackRate(): number {\n return this._playbackRate;\n }\n\n set volume(value: number) {\n this._mountedVideos.forEach((video) => {\n video.volume = value;\n });\n this._volume = value;\n }\n\n get volume(): number {\n this._mountedVideos.forEach((video) => {\n this._volume = video.volume;\n });\n return this._volume;\n }\n\n set loop(value: boolean) {\n this._mountedVideos.forEach((video) => {\n video.loop = value;\n });\n this._loop = value;\n }\n\n get loop(): boolean {\n return this._loop;\n }\n\n get currentTime(): number {\n // All videos should be synchronized, so we return the position of the first video.\n return [...this._mountedVideos][0].currentTime;\n }\n\n set currentTime(value: number) {\n this._mountedVideos.forEach((video) => {\n video.currentTime = value;\n });\n }\n\n get preservesPitch(): boolean {\n return this._preservesPitch;\n }\n set preservesPitch(value: boolean) {\n this._mountedVideos.forEach((video) => {\n video.preservesPitch = value;\n });\n this._preservesPitch = value;\n }\n\n get status(): PlayerStatus {\n return this._status;\n }\n\n mountVideoView(video: HTMLVideoElement) {\n this._mountedVideos.add(video);\n this._synchronizeWithFirstVideo(video);\n this._addListeners(video);\n }\n\n unmountVideoView(video: HTMLVideoElement) {\n const mountedVideos = [...this._mountedVideos];\n const mediaElementSources = [...this._audioNodes];\n const videoIndex = mountedVideos.findIndex((value) => value === video);\n const videoPlayingAudio = mountedVideos[0];\n this._mountedVideos.delete(video);\n this._audioNodes.delete(mediaElementSources[videoIndex]);\n\n // If video playing audio has been removed, select a new video to be the audio player by disconnecting it from the mute node.\n if (videoPlayingAudio === video && this._audioNodes.size > 0 && audioContext) {\n const newMainAudioSource = [...this._audioNodes][0];\n newMainAudioSource.disconnect();\n newMainAudioSource.connect(audioContext.destination);\n }\n }\n\n play(): void {\n this._mountedVideos.forEach((video) => {\n video.play();\n });\n this.playing = true;\n }\n pause(): void {\n this._mountedVideos.forEach((video) => {\n video.pause();\n });\n this.playing = false;\n }\n replace(source: VideoSource): void {\n this._mountedVideos.forEach((video) => {\n const uri = getSourceUri(source);\n video.pause();\n if (uri) {\n video.setAttribute('src', uri);\n video.load();\n video.play();\n } else {\n video.removeAttribute('src');\n }\n });\n this.playing = true;\n }\n seekBy(seconds: number): void {\n this._mountedVideos.forEach((video) => {\n video.currentTime += seconds;\n });\n }\n replay(): void {\n this._mountedVideos.forEach((video) => {\n video.currentTime = 0;\n video.play();\n });\n this.playing = true;\n }\n\n _synchronizeWithFirstVideo(video: HTMLVideoElement): void {\n const firstVideo = [...this._mountedVideos][0];\n if (!firstVideo) return;\n video.currentTime = firstVideo.currentTime;\n video.volume = firstVideo.volume;\n video.muted = firstVideo.muted;\n video.playbackRate = firstVideo.playbackRate;\n }\n\n _addListeners(video: HTMLVideoElement): void {\n video.onloadedmetadata = () => {\n if (!audioContext || !zeroGainNode) return;\n const source = audioContext.createMediaElementSource(video);\n this._audioNodes.add(source);\n\n // First mounted video should be connected to the audio context. All other videos have to be muted.\n if (this._audioNodes.size === 1) {\n source.connect(audioContext.destination);\n } else {\n source.connect(zeroGainNode);\n }\n };\n\n video.onplay = () => {\n this.playing = true;\n this._mountedVideos.forEach((mountedVideo) => {\n mountedVideo.play();\n });\n };\n\n video.onpause = () => {\n this.playing = false;\n this._mountedVideos.forEach((mountedVideo) => {\n mountedVideo.pause();\n });\n };\n\n video.onvolumechange = () => {\n this.volume = video.volume;\n this._muted = video.muted;\n };\n\n video.onseeking = () => {\n this._mountedVideos.forEach((mountedVideo) => {\n if (mountedVideo === video || mountedVideo.currentTime === video.currentTime) return;\n mountedVideo.currentTime = video.currentTime;\n });\n };\n\n video.onseeked = () => {\n this._mountedVideos.forEach((mountedVideo) => {\n if (mountedVideo === video || mountedVideo.currentTime === video.currentTime) return;\n mountedVideo.currentTime = video.currentTime;\n });\n };\n\n video.onratechange = () => {\n this._mountedVideos.forEach((mountedVideo) => {\n if (mountedVideo === video || mountedVideo.playbackRate === video.playbackRate) return;\n this._playbackRate = video.playbackRate;\n mountedVideo.playbackRate = video.playbackRate;\n });\n };\n\n video.onerror = () => {\n this._status = 'error';\n };\n\n video.onloadeddata = () => {\n this._status = 'readyToPlay';\n };\n\n video.onwaiting = () => {\n this._status = 'loading';\n };\n }\n\n release(): void {\n console.warn('The `VideoPlayer.release` method is not supported on web');\n }\n addListener(\n eventName: EventName,\n listener: Record[EventName]\n ): void {\n console.warn('The `VideoPlayer.addListener` method is not yet supported on web');\n }\n removeListener(\n eventName: EventName,\n listener: Record[EventName]\n ): void {\n console.warn('The `VideoPlayer.removeListener` method is not yet supported on web');\n }\n removeAllListeners(eventName: never): void {\n console.warn('The `VideoPlayer.removeAllListeners` method is not yet supported on web');\n }\n emit(\n eventName: EventName,\n ...args: Parameters[EventName]>\n ): void {\n console.warn('The `VideoPlayer.emit` method is not yet supported on web');\n }\n}\n\nfunction mapStyles(style: VideoViewProps['style']): React.CSSProperties {\n const flattenedStyles = StyleSheet.flatten(style);\n // Looking through react-native-web source code they also just pass styles directly without further conversions, so it's just a cast.\n return flattenedStyles as React.CSSProperties;\n}\n\nexport function useVideoPlayer(\n source: VideoSource,\n setup?: (player: VideoPlayer) => void\n): VideoPlayer {\n const parsedSource = typeof source === 'string' ? { uri: source } : source;\n\n return useMemo(() => {\n const player = new VideoPlayerWeb(parsedSource);\n setup?.(player);\n return player;\n }, [JSON.stringify(source)]);\n}\n\nfunction getSourceUri(source: VideoSource): string | null {\n if (typeof source == 'string') {\n return source;\n }\n return source?.uri ?? null;\n}\n\nexport const VideoView = forwardRef((props: { player?: VideoPlayerWeb } & VideoViewProps, ref) => {\n const videoRef = useRef(null);\n useImperativeHandle(ref, () => ({\n enterFullscreen: () => {\n if (!props.allowsFullscreen) {\n return;\n }\n videoRef.current?.requestFullscreen();\n },\n exitFullscreen: () => {\n document.exitFullscreen();\n },\n }));\n\n useEffect(() => {\n return () => {\n if (videoRef.current) {\n props.player?.unmountVideoView(videoRef.current);\n }\n };\n }, []);\n\n useEffect(() => {\n if (!props.player || !videoRef.current) {\n return;\n }\n props.player.mountVideoView(videoRef.current);\n return () => {\n if (videoRef.current) {\n props.player?.unmountVideoView(videoRef.current);\n }\n };\n }, [props.player]);\n\n return (\n {\n // This is called with a null value before `player.unmountVideoView` is called,\n // we can't assign null to videoRef if we want to unmount it from the player.\n if (newRef) {\n videoRef.current = newRef;\n }\n }}\n src={getSourceUri(props.player?.src) ?? ''}\n />\n );\n});\n\nexport default VideoView;\n"]} \ No newline at end of file diff --git a/packages/expo-video/build/index.d.ts b/packages/expo-video/build/index.d.ts index 5e6489c2cf2cc..dad6c647430a6 100644 --- a/packages/expo-video/build/index.d.ts +++ b/packages/expo-video/build/index.d.ts @@ -1,5 +1,5 @@ import Video from './NativeVideoModule'; export { VideoView, useVideoPlayer, isPictureInPictureSupported } from './VideoView'; export { Video }; -export { VideoSource } from './VideoView.types'; +export { VideoSource, VideoPlayerEvents } from './VideoView.types'; //# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/packages/expo-video/build/index.d.ts.map b/packages/expo-video/build/index.d.ts.map index 8a5194796325e..4659bd04fc5c2 100644 --- a/packages/expo-video/build/index.d.ts.map +++ b/packages/expo-video/build/index.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,qBAAqB,CAAC;AAExC,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,2BAA2B,EAAE,MAAM,aAAa,CAAC;AACrF,OAAO,EAAE,KAAK,EAAE,CAAC;AACjB,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC"} \ No newline at end of file +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,qBAAqB,CAAC;AAExC,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,2BAA2B,EAAE,MAAM,aAAa,CAAC;AACrF,OAAO,EAAE,KAAK,EAAE,CAAC;AACjB,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC"} \ No newline at end of file diff --git a/packages/expo-video/build/index.js.map b/packages/expo-video/build/index.js.map index 35a4a7de21eaa..33562d87dc0ed 100644 --- a/packages/expo-video/build/index.js.map +++ b/packages/expo-video/build/index.js.map @@ -1 +1 @@ -{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,qBAAqB,CAAC;AAExC,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,2BAA2B,EAAE,MAAM,aAAa,CAAC;AACrF,OAAO,EAAE,KAAK,EAAE,CAAC","sourcesContent":["import Video from './NativeVideoModule';\n\nexport { VideoView, useVideoPlayer, isPictureInPictureSupported } from './VideoView';\nexport { Video };\nexport { VideoSource } from './VideoView.types';\n"]} \ No newline at end of file +{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,qBAAqB,CAAC;AAExC,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,2BAA2B,EAAE,MAAM,aAAa,CAAC;AACrF,OAAO,EAAE,KAAK,EAAE,CAAC","sourcesContent":["import Video from './NativeVideoModule';\n\nexport { VideoView, useVideoPlayer, isPictureInPictureSupported } from './VideoView';\nexport { Video };\nexport { VideoSource, VideoPlayerEvents } from './VideoView.types';\n"]} \ No newline at end of file diff --git a/packages/expo-video/ios/Enums/PlayerStatus.swift b/packages/expo-video/ios/Enums/PlayerStatus.swift new file mode 100644 index 0000000000000..6af69ca81b7b7 --- /dev/null +++ b/packages/expo-video/ios/Enums/PlayerStatus.swift @@ -0,0 +1,10 @@ +// Copyright 2024-present 650 Industries. All rights reserved. + +import ExpoModulesCore + +internal enum PlayerStatus: String, Enumerable { + case idle + case loading + case readyToPlay + case error +} diff --git a/packages/expo-video/ios/Records/PlaybackError.swift b/packages/expo-video/ios/Records/PlaybackError.swift new file mode 100644 index 0000000000000..e4ec06f4fc073 --- /dev/null +++ b/packages/expo-video/ios/Records/PlaybackError.swift @@ -0,0 +1,10 @@ +// Copyright 2024-present 650 Industries. All rights reserved. + +import Foundation +import ExpoModulesCore + +internal struct PlaybackError: Record { + @Field + // swiftlint:disable:next redundant_optional_initialization - Initialization with nil is necessary + var message: String? = nil +} diff --git a/packages/expo-video/ios/Records/VolumeEvent.swift b/packages/expo-video/ios/Records/VolumeEvent.swift new file mode 100644 index 0000000000000..76af45e2af326 --- /dev/null +++ b/packages/expo-video/ios/Records/VolumeEvent.swift @@ -0,0 +1,14 @@ +// Copyright 2024-present 650 Industries. All rights reserved. + +import Foundation +import ExpoModulesCore + +// swiftlint:disable redundant_optional_initialization - Initialization with nil is necessary +internal struct VolumeEvent: Record { + @Field + var volume: Float? = nil + + @Field + var isMuted: Bool? = nil +} +// swiftlint:enable redundant_optional_initialization diff --git a/packages/expo-video/ios/VideoExceptions.swift b/packages/expo-video/ios/VideoExceptions.swift index 342feebb255e0..45b9a6cb199f1 100644 --- a/packages/expo-video/ios/VideoExceptions.swift +++ b/packages/expo-video/ios/VideoExceptions.swift @@ -19,3 +19,15 @@ internal class DRMLoadException: GenericException { "Failed to decrypt the video stream: \(param ?? "unknown")" } } + +internal class PlayerException: GenericException { + override var reason: String { + "Failed to initialise the player: \(param ?? "unknown")" + } +} + +internal class PlayerItemLoadException: GenericException { + override var reason: String { + "Failed to load the player item: \(param ?? "unknown")" + } +} diff --git a/packages/expo-video/ios/VideoModule.swift b/packages/expo-video/ios/VideoModule.swift index a2d96f84ccfdf..4af81310ba8e0 100644 --- a/packages/expo-video/ios/VideoModule.swift +++ b/packages/expo-video/ios/VideoModule.swift @@ -81,30 +81,24 @@ public final class VideoModule: Module { let player = AVPlayer() let videoPlayer = VideoPlayer(player) - if let url = source.uri { - let asset = AVURLAsset(url: url) - - if let drm = source.drm { - try drm.type.assertIsSupported() - videoPlayer.contentKeyManager.addContentKeyRequest(videoSource: source, asset: asset) - } - let playerItem = AVPlayerItem(asset: asset) - player.replaceCurrentItem(with: playerItem) - } - + try videoPlayer.replaceCurrentItem(with: source) player.pause() return videoPlayer } Property("playing") { player -> Bool in - return player.pointer.timeControlStatus == .playing + return player.isPlaying } Property("muted") { player -> Bool in - return player.pointer.isMuted + return player.isMuted } .set { (player, muted: Bool) in - player.pointer.isMuted = muted + player.isMuted = muted + } + + Property("currentTime") { player -> Double in + return player.pointer.currentTime().seconds } Property("staysActiveInBackground") { player -> Bool in @@ -145,11 +139,15 @@ public final class VideoModule: Module { player.preservesPitch = preservesPitch } + Property("status") { player -> PlayerStatus in + return player.status + } + Property("volume") { player -> Float in - return player.pointer.volume + return player.volume } .set { (player, volume: Float) in - player.pointer.volume = volume + player.volume = volume } Function("play") { player in diff --git a/packages/expo-video/ios/VideoPlayer.swift b/packages/expo-video/ios/VideoPlayer.swift index c3068526d3e60..3c9b66e36b1b6 100644 --- a/packages/expo-video/ios/VideoPlayer.swift +++ b/packages/expo-video/ios/VideoPlayer.swift @@ -4,23 +4,22 @@ import AVFoundation import MediaPlayer import ExpoModulesCore -internal final class VideoPlayer: SharedRef, Hashable { +internal final class VideoPlayer: SharedRef, Hashable, VideoPlayerObserverDelegate { lazy var contentKeyManager = ContentKeyManager() + var observer: VideoPlayerObserver? - var loop = false { - didSet { - applyIsLooping() - } - } - + var loop = false + private(set) var isPlaying = false + private(set) var status: PlayerStatus = .idle var playbackRate: Float = 1.0 { didSet { + if oldValue != playbackRate { + self.emit(event: "playbackRateChange", arguments: playbackRate, oldValue) + } if #available(iOS 16.0, *) { pointer.defaultRate = playbackRate } - if pointer.rate != 0 { - pointer.rate = playbackRate - } + pointer.rate = playbackRate } } @@ -40,31 +39,37 @@ internal final class VideoPlayer: SharedRef, Hashable { } } - private var playerItemObserver: NSObjectProtocol? - private var playerRateObserver: NSObjectProtocol? - - override init(_ pointer: AVPlayer) { - super.init(pointer) - NowPlayingManager.shared.registerPlayer(pointer) - VideoManager.shared.register(videoPlayer: self) + var volume: Float = 1.0 { + didSet { + if oldValue != volume { + let oldVolumeEvent = VolumeEvent(volume: oldValue, isMuted: isMuted) + let newVolumeEvent = VolumeEvent(volume: volume, isMuted: isMuted) - playerRateObserver = pointer.observe(\.rate, options: [.new]) {[weak self] _, change in - guard let newRate = change.newValue, let self else { - return + self.emit(event: "volumeChange", arguments: newVolumeEvent, oldVolumeEvent) } + pointer.volume = volume + } + } - if #available(iOS 16.0, *) { - if self.pointer.defaultRate != playbackRate { - // User changed the playback speed in the native controls. Update the desiredRate variable - self.playbackRate = self.pointer.defaultRate - } - } else if newRate != 0 && newRate != playbackRate { - // On iOS < 16 play() method always returns the reate to 1.0, we have to keep resetting it back to desiredRate - self.pointer.rate = playbackRate + var isMuted: Bool = false { + didSet { + if oldValue != isMuted { + let oldVolumeEvent = VolumeEvent(volume: volume, isMuted: oldValue) + let newVolumeEvent = VolumeEvent(volume: volume, isMuted: isMuted) + + self.emit(event: "volumeChange", arguments: newVolumeEvent.isMuted, oldVolumeEvent.isMuted) } + pointer.isMuted = isMuted } } + override init(_ pointer: AVPlayer) { + super.init(pointer) + observer = VideoPlayerObserver(player: pointer, delegate: self) + NowPlayingManager.shared.registerPlayer(pointer) + VideoManager.shared.register(videoPlayer: self) + } + deinit { NowPlayingManager.shared.unregisterPlayer(pointer) VideoManager.shared.unregister(videoPlayer: self) @@ -77,37 +82,19 @@ internal final class VideoPlayer: SharedRef, Hashable { let url = videoSource.uri else { pointer.replaceCurrentItem(with: nil) - applyIsLooping() return } let asset = AVURLAsset(url: url) - let playerItem = AVPlayerItem(asset: asset) - playerItem.audioTimePitchAlgorithm = preservesPitch ? .spectral : .varispeed + let playerItem = VideoPlayerItem(asset: asset, videoSource: videoSource) if let drm = videoSource.drm { try drm.type.assertIsSupported() contentKeyManager.addContentKeyRequest(videoSource: videoSource, asset: asset) } + playerItem.audioTimePitchAlgorithm = preservesPitch ? .spectral : .varispeed pointer.replaceCurrentItem(with: playerItem) - applyIsLooping() - } - - private func applyIsLooping() { - NotificationCenter.default.removeObserver(playerItemObserver) - playerItemObserver = nil - - if let currentItem = pointer.currentItem, loop { - playerItemObserver = NotificationCenter.default.addObserver( - forName: NSNotification.Name.AVPlayerItemDidPlayToEndTime, - object: pointer.currentItem, - queue: nil - ) { [weak self] _ in - self?.pointer.seek(to: .zero) - self?.pointer.play() - } - } } /** @@ -127,6 +114,54 @@ internal final class VideoPlayer: SharedRef, Hashable { }) } + // MARK: - VideoPlayerObserverDelegate + + func onStatusChanged(player: AVPlayer, oldStatus: PlayerStatus?, newStatus: PlayerStatus, error: Exception?) { + let errorRecord = error != nil ? PlaybackError(message: error?.localizedDescription) : nil + self.emit(event: "statusChange", arguments: newStatus.rawValue, oldStatus?.rawValue, errorRecord) + status = newStatus + } + + func onIsPlayingChanged(player: AVPlayer, oldIsPlaying: Bool?, newIsPlaying: Bool) { + self.emit(event: "playingChange", arguments: newIsPlaying, oldIsPlaying) + isPlaying = newIsPlaying + } + + func onRateChanged(player: AVPlayer, oldRate: Float?, newRate: Float) { + if #available(iOS 16.0, *) { + if player.defaultRate != playbackRate { + // User changed the playback speed in the native controls. Update the desiredRate variable + playbackRate = player.defaultRate + } + } else if newRate != 0 && newRate != playbackRate { + // On iOS < 16 play() method always returns the rate to 1.0, we have to keep resetting it back to desiredRate + // iOS < 16 uses an older player UI, so we don't have to worry about changes to the rate that come from the player UI + pointer.rate = playbackRate + } + } + + func onVolumeChanged(player: AVPlayer, oldVolume: Float?, newVolume: Float) { + volume = newVolume + } + + func onIsMutedChanged(player: AVPlayer, oldIsMuted: Bool?, newIsMuted: Bool) { + isMuted = newIsMuted + } + + func onPlayedToEnd(player: AVPlayer) { + self.emit(event: "playToEnd") + if loop { + self.pointer.seek(to: .zero) + self.pointer.play() + } + } + + func onItemChanged(player: AVPlayer, oldVideoPlayerItem: VideoPlayerItem?, newVideoPlayerItem: VideoPlayerItem?) { + self.emit(event: "sourceChange", arguments: newVideoPlayerItem?.videoSource, oldVideoPlayerItem?.videoSource) + } + + // MARK: - Hashable + func hash(into hasher: inout Hasher) { hasher.combine(ObjectIdentifier(self)) } diff --git a/packages/expo-video/ios/VideoPlayerItem.swift b/packages/expo-video/ios/VideoPlayerItem.swift new file mode 100644 index 0000000000000..f3e4bec5daf77 --- /dev/null +++ b/packages/expo-video/ios/VideoPlayerItem.swift @@ -0,0 +1,11 @@ +// Copyright 2024-present 650 Industries. All rights reserved. + +import AVFoundation + +class VideoPlayerItem: AVPlayerItem { + let videoSource: VideoSource + init(asset: AVAsset, videoSource: VideoSource) { + self.videoSource = videoSource + super.init(asset: asset, automaticallyLoadedAssetKeys: nil) + } +} diff --git a/packages/expo-video/ios/VideoPlayerObserver.swift b/packages/expo-video/ios/VideoPlayerObserver.swift new file mode 100644 index 0000000000000..dce767a08f3ad --- /dev/null +++ b/packages/expo-video/ios/VideoPlayerObserver.swift @@ -0,0 +1,211 @@ +// Copyright 2024-present 650 Industries. All rights reserved. + +import Foundation +import ExpoModulesCore +import AVFoundation + +protocol VideoPlayerObserverDelegate: AnyObject { + func onStatusChanged(player: AVPlayer, oldStatus: PlayerStatus?, newStatus: PlayerStatus, error: Exception?) + func onIsPlayingChanged(player: AVPlayer, oldIsPlaying: Bool?, newIsPlaying: Bool) + func onRateChanged(player: AVPlayer, oldRate: Float?, newRate: Float) + func onVolumeChanged(player: AVPlayer, oldVolume: Float?, newVolume: Float) + func onPlayedToEnd(player: AVPlayer) + func onItemChanged(player: AVPlayer, oldVideoPlayerItem: VideoPlayerItem?, newVideoPlayerItem: VideoPlayerItem?) + func onIsMutedChanged(player: AVPlayer, oldIsMuted: Bool?, newIsMuted: Bool) +} + +class VideoPlayerObserver { + let player: AVPlayer + weak var delegate: VideoPlayerObserverDelegate? + private var currentItem: VideoPlayerItem? + + private var isPlaying: Bool = false { + didSet { + if oldValue != isPlaying { + delegate?.onIsPlayingChanged(player: player, oldIsPlaying: oldValue, newIsPlaying: isPlaying) + } + } + } + private var error: Exception? + private var status: PlayerStatus = .idle { + didSet { + if oldValue != status { + delegate?.onStatusChanged(player: player, oldStatus: oldValue, newStatus: status, error: error) + } + } + } + + private var playerItemObserver: NSObjectProtocol? + private var playerRateObserver: NSKeyValueObservation? + + // Player observers + private var playerStatusObserver: NSKeyValueObservation? + private var playerTimeControlStatusObserver: NSKeyValueObservation? + private var playerVolumeObserver: NSKeyValueObservation? + private var playerCurrentItemObserver: NSKeyValueObservation? + private var playerIsMutedObserver: NSKeyValueObservation? + + // Current player item observers + private var playbackBufferEmptyObserver: NSKeyValueObservation? + private var playerItemStatusObserver: NSKeyValueObservation? + private var playbackLikelyToKeepUpObserver: NSKeyValueObservation? + + init(player: AVPlayer, delegate: VideoPlayerObserverDelegate) { + self.player = player + self.delegate = delegate + initializePlayerObservers() + } + + deinit { + invalidatePlayerObservers() + invalidateCurrentPlayerItemObservers() + } + + private func initializePlayerObservers() { + playerRateObserver = player.observe(\.rate, options: [.initial, .new, .old], changeHandler: onPlayerRateChanged) + playerStatusObserver = player.observe(\.status, options: [.initial, .new, .old], changeHandler: onPlayerStatusChanged) + playerTimeControlStatusObserver = player.observe(\.timeControlStatus, options: [.new, .old], changeHandler: onTimeControlStatusChanged) + playerVolumeObserver = player.observe(\.volume, options: [.initial, .new, .old], changeHandler: onPlayerVolumeChanged) + playerIsMutedObserver = player.observe(\.isMuted, options: [.initial, .new, .old], changeHandler: onPlayerIsMutedChanged) + playerCurrentItemObserver = player.observe(\.currentItem, options: [.initial, .new], changeHandler: onPlayerCurrentItemChanged) + } + + private func invalidatePlayerObservers() { + playerRateObserver?.invalidate() + playerStatusObserver?.invalidate() + playerTimeControlStatusObserver?.invalidate() + playerVolumeObserver?.invalidate() + playerIsMutedObserver?.invalidate() + playerCurrentItemObserver?.invalidate() + } + + private func initializeCurrentPlayerItemObservers(player: AVPlayer, playerItem: AVPlayerItem) { + // Dual question marks due to the change wrapping an optional value as an optional Optional> + playbackBufferEmptyObserver = playerItem.observe(\.isPlaybackBufferEmpty, changeHandler: onIsBufferEmptyChanged) + playbackLikelyToKeepUpObserver = playerItem.observe(\.isPlaybackLikelyToKeepUp, changeHandler: onPlayerLikelyToKeepUpChanged) + playerItemStatusObserver = playerItem.observe(\.status, options: [.initial, .new], changeHandler: onItemStatusChanged) + + playerItemObserver = NotificationCenter.default.addObserver( + forName: NSNotification.Name.AVPlayerItemDidPlayToEndTime, + object: playerItem, + queue: nil + ) { [weak self] _ in + self?.delegate?.onPlayedToEnd(player: player) + } + } + + private func invalidateCurrentPlayerItemObservers() { + playbackLikelyToKeepUpObserver?.invalidate() + playbackBufferEmptyObserver?.invalidate() + playerItemStatusObserver?.invalidate() + NotificationCenter.default.removeObserver(playerItemObserver) + } + + // MARK: - VideoPlayerObserverDelegate + + private func onPlayerCurrentItemChanged(_ player: AVPlayer, _ change: NSKeyValueObservedChange) { + let newPlayerItem = change.newValue + + invalidateCurrentPlayerItemObservers() + + if let videoPlayerItem = newPlayerItem as? VideoPlayerItem { + initializeCurrentPlayerItemObservers(player: player, playerItem: videoPlayerItem) + delegate?.onItemChanged(player: player, oldVideoPlayerItem: currentItem, newVideoPlayerItem: videoPlayerItem) + currentItem = videoPlayerItem + return + } + + if newPlayerItem == nil { + delegate?.onItemChanged(player: player, oldVideoPlayerItem: currentItem, newVideoPlayerItem: nil) + status = .idle + } else { + log.warn( + "VideoPlayer's AVPlayer has been initialized with a `AVPlayerItem` instead of a `VideoPlayerItem`." + + "Always use `VideoPlayerItem` as a wrapper for media played in `VideoPlayer`." + ) + } + currentItem = nil + } + + private func onItemStatusChanged(_ playerItem: AVPlayerItem, _ change: NSKeyValueObservedChange) { + if player.status != .failed { + error = nil + } + + switch playerItem.status { + case .unknown: + status = .loading + case .failed: + error = PlayerItemLoadException(playerItem.error?.localizedDescription) + status = .error + case .readyToPlay: + if playerItem.isPlaybackBufferEmpty { + status = .loading + } else { + status = .readyToPlay + } + } + } + + private func onPlayerStatusChanged(_ player: AVPlayer, _ change: NSKeyValueObservedChange) { + if player.currentItem?.status != .failed { + error = nil + } + + if player.status == .failed { + error = PlayerException(player.error?.localizedDescription) + status = .error + } + } + + private func onTimeControlStatusChanged(_ player: AVPlayer, _ change: NSKeyValueObservedChange) { + // iOS changes timeControlStatus after an error, so we need to check for errors. + if player.status == .failed || player.currentItem?.status == .failed { + isPlaying = false + return + } + error = nil + + if player.timeControlStatus != .waitingToPlayAtSpecifiedRate && player.status == .readyToPlay && currentItem?.isPlaybackBufferEmpty != true { + status = .readyToPlay + } else if player.timeControlStatus == .waitingToPlayAtSpecifiedRate { + status = .loading + } + + if isPlaying != (player.timeControlStatus == .playing) { + isPlaying = player.timeControlStatus == .playing + } + } + + private func onIsBufferEmptyChanged(_ playerItem: AVPlayerItem, _ change: NSKeyValueObservedChange) { + if playerItem.isPlaybackBufferEmpty { + status = .loading + } + } + + private func onPlayerLikelyToKeepUpChanged(_ playerItem: AVPlayerItem, _ change: NSKeyValueObservedChange) { + if !playerItem.isPlaybackLikelyToKeepUp && playerItem.isPlaybackBufferEmpty { + status = .loading + } else if playerItem.isPlaybackLikelyToKeepUp { + status = .readyToPlay + } + } + + private func onPlayerRateChanged(_ player: AVPlayer, _ change: NSKeyValueObservedChange) { + if let newRate = change.newValue, change.oldValue != change.newValue { + delegate?.onRateChanged(player: player, oldRate: change.oldValue, newRate: newRate) + } + } + + private func onPlayerVolumeChanged(_ player: AVPlayer, _ change: NSKeyValueObservedChange) { + if let newVolume = change.newValue, change.oldValue != change.newValue { + delegate?.onVolumeChanged(player: player, oldVolume: change.oldValue, newVolume: newVolume) + } + } + + private func onPlayerIsMutedChanged(_ player: AVPlayer, _ change: NSKeyValueObservedChange) { + if let newIsMuted = change.newValue, change.oldValue != change.newValue { + delegate?.onIsMutedChanged(player: player, oldIsMuted: change.oldValue, newIsMuted: newIsMuted) + } + } +} diff --git a/packages/expo-video/src/NativeVideoModule.web.ts b/packages/expo-video/src/NativeVideoModule.web.ts new file mode 100644 index 0000000000000..2d1ec238274a0 --- /dev/null +++ b/packages/expo-video/src/NativeVideoModule.web.ts @@ -0,0 +1 @@ +export default () => {}; diff --git a/packages/expo-video/src/VideoView.tsx b/packages/expo-video/src/VideoView.tsx index 825b982b4d409..03756f8d24a13 100644 --- a/packages/expo-video/src/VideoView.tsx +++ b/packages/expo-video/src/VideoView.tsx @@ -13,13 +13,17 @@ import NativeVideoModule from './NativeVideoModule'; import NativeVideoView from './NativeVideoView'; import { VideoPlayer, VideoSource, VideoViewProps } from './VideoView.types'; -export function useVideoPlayer(source: VideoSource): VideoPlayer { +export function useVideoPlayer( + source: VideoSource, + setup?: (player: VideoPlayer) => void +): VideoPlayer { const parsedSource = typeof source === 'string' ? { uri: source } : source; - return useReleasingSharedObject( - () => new NativeVideoModule.VideoPlayer(parsedSource), - [JSON.stringify(parsedSource)] - ); + return useReleasingSharedObject(() => { + const player = new NativeVideoModule.VideoPlayer(parsedSource); + setup?.(player); + return player; + }, [JSON.stringify(parsedSource)]); } /** diff --git a/packages/expo-video/src/VideoView.types.ts b/packages/expo-video/src/VideoView.types.ts index cf9d411eefd95..1203ca2ece3d5 100644 --- a/packages/expo-video/src/VideoView.types.ts +++ b/packages/expo-video/src/VideoView.types.ts @@ -4,7 +4,7 @@ import { ViewProps } from 'react-native'; /** * A class that represents an instance of the video player. */ -export declare class VideoPlayer extends SharedObject { +export declare class VideoPlayer extends SharedObject { /** * Boolean value whether the player is currently playing. * > This property is get-only, use `play` and `pause` methods to control the playback. @@ -51,6 +51,12 @@ export declare class VideoPlayer extends SharedObject { */ playbackRate: number; + /** + * Indicates the current status of the player. + * > This property is get-only + */ + status: PlayerStatus; + /** * Determines whether the player should continue playing after the app enters the background. * @default false @@ -87,12 +93,21 @@ export declare class VideoPlayer extends SharedObject { /** * Describes how a video should be scaled to fit in a container. - * 'contain': The video maintains its aspect ratio and fits inside the container, with possible letterboxing/pillarboxing. - * 'cover': The video maintains its aspect ratio and covers the entire container, potentially cropping some portions. - * 'fill': The video stretches/squeezes to completely fill the container, potentially causing distortion. + * - `contain`: The video maintains its aspect ratio and fits inside the container, with possible letterboxing/pillarboxing. + * - `cover`: The video maintains its aspect ratio and covers the entire container, potentially cropping some portions. + * - `fill`: The video stretches/squeezes to completely fill the container, potentially causing distortion. */ type VideoContentFit = 'contain' | 'cover' | 'fill'; +/** + * Describes the current status of the player. + * - `idle`: The player is not playing or loading any videos. + * - `loading`: The player is loading video data from the provided source + * - `readyToPlay`: The player has loaded enough data to start playing or to continue playback. + * - `error`: The player has encountered an error while loading or playing the video. + */ +export type PlayerStatus = 'idle' | 'loading' | 'readyToPlay' | 'error'; + export interface VideoViewProps extends ViewProps { /** * A player instance – use `useVideoPlayer()` to create one. @@ -103,27 +118,27 @@ export interface VideoViewProps extends ViewProps { * Determines whether native controls should be displayed or not. * @default true */ - nativeControls: boolean | undefined; + nativeControls?: boolean; /** * Describes how the video should be scaled to fit in the container. * Options are 'contain', 'cover', and 'fill'. * @default 'contain' */ - contentFit: VideoContentFit | undefined; + contentFit?: VideoContentFit; /** * Determines whether fullscreen mode is allowed or not. * @default true */ - allowsFullscreen: boolean | undefined; + allowsFullscreen?: boolean; /** * Determines whether the timecodes should be displayed or not. * @default true * @platform ios */ - showsTimecodes: boolean | undefined; + showsTimecodes?: boolean; /** * Determines whether the player allows the user to skip media content. @@ -131,14 +146,14 @@ export interface VideoViewProps extends ViewProps { * @platform android * @platform ios */ - requiresLinearPlayback: boolean | undefined; + requiresLinearPlayback?: boolean; /** * Determines the position offset of the video inside the container. * @default { dx: 0, dy: 0 } * @platform ios */ - contentPosition: { dx?: number; dy?: number } | undefined; + contentPosition?: { dx?: number; dy?: number }; /** * A callback to call after the video player enters Picture in Picture (PiP) mode. @@ -215,3 +230,48 @@ type DRMOptions = { }; export type VideoSource = string | { uri: string; drm?: DRMOptions } | null; + +/** + * Handlers for events which can be emitted by the player. + */ +export type VideoPlayerEvents = { + /** + * Handler for an event emitted when the status of the player changes. + */ + statusChange: (newStatus: PlayerStatus, oldStatus: PlayerStatus, error: PlayerError) => void; + /** + * Handler for an event emitted when the player starts or stops playback. + */ + playingChange: (newIsPlaying: boolean, oldIsPlaying: boolean) => void; + /** + * Handler for an event emitted when the `playbackRate` property of the player changes. + */ + playbackRateChange: (newPlaybackRate: number, oldPlaybackRate: number) => void; + /** + * Handler for an event emitted when the `volume` property of the player changes. + */ + volumeChange: (newVolume: VolumeEvent, oldVolume: VolumeEvent) => void; + /** + * Handler for an event emitted when the player plays to the end of the current source. + */ + playToEnd: () => void; + /** + * Handler for an event emitted when the current media source of the player changes. + */ + sourceChange: (newSource: VideoSource, previousSource: VideoSource) => void; +}; + +/** + * Contains information about any errors that the player encountered during the playback + */ +type PlayerError = { + message: string; +}; + +/** + * Contains information about the current volume and whether the player is muted. + */ +type VolumeEvent = { + volume: number; + isMuted: boolean; +}; diff --git a/packages/expo-video/src/VideoView.web.tsx b/packages/expo-video/src/VideoView.web.tsx index 4b6ad41748fb7..5c6758b7cde6e 100644 --- a/packages/expo-video/src/VideoView.web.tsx +++ b/packages/expo-video/src/VideoView.web.tsx @@ -1,8 +1,7 @@ -import React, { useEffect, useRef, forwardRef, useImperativeHandle } from 'react'; +import React, { useEffect, useRef, forwardRef, useImperativeHandle, useMemo } from 'react'; import { StyleSheet } from 'react-native'; -import { VideoPlayer, VideoViewProps } from './VideoView.types'; - +import { PlayerStatus, VideoPlayer, VideoSource, VideoViewProps } from './VideoView.types'; /** * This audio context is used to mute all but one video when multiple video views are playing from one player simultaneously. * Using audio context nodes allows muting videos without displaying the mute icon in the video player. @@ -17,13 +16,12 @@ if (audioContext && zeroGainNode) { "Couldn't create AudioContext, this might affect the audio playback when using multiple video views with the same player." ); } - class VideoPlayerWeb implements VideoPlayer { - constructor(source: string | null = null) { + constructor(source: VideoSource) { this.src = source; } - src: string | null = null; + src: VideoSource = null; _mountedVideos: Set = new Set(); _audioNodes: Set = new Set(); playing: boolean = false; @@ -32,6 +30,7 @@ class VideoPlayerWeb implements VideoPlayer { _loop: boolean = false; _playbackRate: number = 1.0; _preservesPitch: boolean = true; + _status: PlayerStatus = 'idle'; staysActiveInBackground: boolean = false; // Not supported on web. Dummy to match the interface. set muted(value: boolean) { @@ -100,6 +99,10 @@ class VideoPlayerWeb implements VideoPlayer { this._preservesPitch = value; } + get status(): PlayerStatus { + return this._status; + } + mountVideoView(video: HTMLVideoElement) { this._mountedVideos.add(video); this._synchronizeWithFirstVideo(video); @@ -134,12 +137,17 @@ class VideoPlayerWeb implements VideoPlayer { }); this.playing = false; } - replace(source: string): void { + replace(source: VideoSource): void { this._mountedVideos.forEach((video) => { + const uri = getSourceUri(source); video.pause(); - video.setAttribute('src', source); - video.load(); - video.play(); + if (uri) { + video.setAttribute('src', uri); + video.load(); + video.play(); + } else { + video.removeAttribute('src'); + } }); this.playing = true; } @@ -219,6 +227,18 @@ class VideoPlayerWeb implements VideoPlayer { mountedVideo.playbackRate = video.playbackRate; }); }; + + video.onerror = () => { + this._status = 'error'; + }; + + video.onloadeddata = () => { + this._status = 'readyToPlay'; + }; + + video.onwaiting = () => { + this._status = 'loading'; + }; } release(): void { @@ -253,11 +273,24 @@ function mapStyles(style: VideoViewProps['style']): React.CSSProperties { return flattenedStyles as React.CSSProperties; } -export function useVideoPlayer(source: string | null = null): VideoPlayer { - return React.useMemo(() => { - return new VideoPlayerWeb(source); - // should this not include source? - }, []); +export function useVideoPlayer( + source: VideoSource, + setup?: (player: VideoPlayer) => void +): VideoPlayer { + const parsedSource = typeof source === 'string' ? { uri: source } : source; + + return useMemo(() => { + const player = new VideoPlayerWeb(parsedSource); + setup?.(player); + return player; + }, [JSON.stringify(source)]); +} + +function getSourceUri(source: VideoSource): string | null { + if (typeof source == 'string') { + return source; + } + return source?.uri ?? null; } export const VideoView = forwardRef((props: { player?: VideoPlayerWeb } & VideoViewProps, ref) => { @@ -310,7 +343,7 @@ export const VideoView = forwardRef((props: { player?: VideoPlayerWeb } & VideoV videoRef.current = newRef; } }} - src={props.player?.src ?? ''} + src={getSourceUri(props.player?.src) ?? ''} /> ); }); diff --git a/packages/expo-video/src/index.ts b/packages/expo-video/src/index.ts index c98e2e1ca61ea..9ddb43699b27e 100644 --- a/packages/expo-video/src/index.ts +++ b/packages/expo-video/src/index.ts @@ -2,4 +2,4 @@ import Video from './NativeVideoModule'; export { VideoView, useVideoPlayer, isPictureInPictureSupported } from './VideoView'; export { Video }; -export { VideoSource } from './VideoView.types'; +export { VideoSource, VideoPlayerEvents } from './VideoView.types';