expo-video 1.1.3 → 1.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/android/build.gradle +2 -2
- package/build/VideoPlayer.web.d.ts +1 -0
- package/build/VideoPlayer.web.d.ts.map +1 -1
- package/build/VideoPlayer.web.js +1 -0
- package/build/VideoPlayer.web.js.map +1 -1
- package/build/VideoView.types.d.ts +29 -1
- package/build/VideoView.types.d.ts.map +1 -1
- package/build/VideoView.types.js.map +1 -1
- package/ios/NowPlayingManager.swift +54 -35
- package/ios/Records/VideoMetadata.swift +13 -0
- package/ios/Records/VideoSource.swift +8 -3
- package/ios/VideoManager.swift +38 -9
- package/ios/VideoModule.swift +8 -1
- package/ios/VideoPlayer.swift +22 -7
- package/ios/VideoPlayerObserver.swift +83 -11
- package/ios/VideoView.swift +3 -8
- package/package.json +2 -2
- package/src/VideoPlayer.web.tsx +1 -0
- package/src/VideoView.types.ts +38 -2
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,16 @@
|
|
|
10
10
|
|
|
11
11
|
### 💡 Others
|
|
12
12
|
|
|
13
|
+
## 1.1.5 — 2024-04-26
|
|
14
|
+
|
|
15
|
+
### 🎉 New features
|
|
16
|
+
|
|
17
|
+
- [iOS] Add support for customizing the now playing notification. ([#28386](https://github.com/expo/expo/pull/28386) by [@behenate](https://github.com/behenate))
|
|
18
|
+
|
|
19
|
+
## 1.1.4 — 2024-04-25
|
|
20
|
+
|
|
21
|
+
_This version does not introduce any user-facing changes._
|
|
22
|
+
|
|
13
23
|
## 1.1.3 — 2024-04-24
|
|
14
24
|
|
|
15
25
|
_This version does not introduce any user-facing changes._
|
package/android/build.gradle
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
apply plugin: 'com.android.library'
|
|
2
2
|
|
|
3
3
|
group = 'host.exp.exponent'
|
|
4
|
-
version = '1.1.
|
|
4
|
+
version = '1.1.5'
|
|
5
5
|
|
|
6
6
|
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
|
|
7
7
|
apply from: expoModulesCorePlugin
|
|
@@ -14,7 +14,7 @@ android {
|
|
|
14
14
|
namespace "expo.modules.video"
|
|
15
15
|
defaultConfig {
|
|
16
16
|
versionCode 1
|
|
17
|
-
versionName '1.1.
|
|
17
|
+
versionName '1.1.5'
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
|
|
@@ -14,6 +14,7 @@ export declare class VideoPlayerWeb extends globalThis.expo.SharedObject<VideoPl
|
|
|
14
14
|
_preservesPitch: boolean;
|
|
15
15
|
_status: VideoPlayerStatus;
|
|
16
16
|
staysActiveInBackground: boolean;
|
|
17
|
+
showNowPlayingNotification: boolean;
|
|
17
18
|
set muted(value: boolean);
|
|
18
19
|
get muted(): boolean;
|
|
19
20
|
set playbackRate(value: number);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VideoPlayer.web.d.ts","sourceRoot":"","sources":["../src/VideoPlayer.web.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEnG,wBAAgB,cAAc,CAC5B,MAAM,EAAE,WAAW,EACnB,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,GACpC,WAAW,CAQb;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,GAAG,IAAI,CAK/D;AAED,qBAAa,cACX,SAAQ,UAAU,CAAC,IAAI,CAAC,YAAY,CAAC,iBAAiB,CACtD,YAAW,WAAW;gBAEV,MAAM,EAAE,WAAW;IAK/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,iBAAiB,CAAU;IACpC,uBAAuB,EAAE,OAAO,CAAS;
|
|
1
|
+
{"version":3,"file":"VideoPlayer.web.d.ts","sourceRoot":"","sources":["../src/VideoPlayer.web.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEnG,wBAAgB,cAAc,CAC5B,MAAM,EAAE,WAAW,EACnB,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,GACpC,WAAW,CAQb;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,GAAG,IAAI,CAK/D;AAED,qBAAa,cACX,SAAQ,UAAU,CAAC,IAAI,CAAC,YAAY,CAAC,iBAAiB,CACtD,YAAW,WAAW;gBAEV,MAAM,EAAE,WAAW;IAK/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,iBAAiB,CAAU;IACpC,uBAAuB,EAAE,OAAO,CAAS;IACzC,0BAA0B,EAAE,OAAO,CAAS;IAE5C,IAAI,KAAK,CAAC,KAAK,EAAE,OAAO,EAKvB;IAED,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;IAED,IAAI,cAAc,CAAC,KAAK,EAAE,OAAO,EAKhC;IAED,IAAI,MAAM,IAAI,iBAAiB,CAE9B;IAED,cAAc,CAAC,KAAK,EAAE,gBAAgB;IAMtC,gBAAgB,CAAC,KAAK,EAAE,gBAAgB;IAIxC,cAAc,CACZ,YAAY,EAAE,YAAY,EAC1B,YAAY,EAAE,QAAQ,EACtB,eAAe,EAAE,2BAA2B,GAC3C,IAAI;IAYP,gBAAgB,CACd,KAAK,EAAE,gBAAgB,EACvB,YAAY,EAAE,YAAY,EAC1B,eAAe,EAAE,2BAA2B;IAe9C,IAAI,IAAI,IAAI;IAOZ,KAAK,IAAI,IAAI;IAOb,OAAO,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI;IAelC,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAM7B,MAAM,IAAI,IAAI;IAQd,0BAA0B,CAAC,KAAK,EAAE,gBAAgB,GAAG,IAAI;IAezD,aAAa,CAAC,KAAK,EAAE,gBAAgB,GAAG,IAAI;CA0D7C"}
|
package/build/VideoPlayer.web.js
CHANGED
|
@@ -29,6 +29,7 @@ export class VideoPlayerWeb extends globalThis.expo.SharedObject {
|
|
|
29
29
|
_preservesPitch = true;
|
|
30
30
|
_status = 'idle';
|
|
31
31
|
staysActiveInBackground = false; // Not supported on web. Dummy to match the interface.
|
|
32
|
+
showNowPlayingNotification = false; // Not supported on web. Dummy to match the interface.
|
|
32
33
|
set muted(value) {
|
|
33
34
|
this._mountedVideos.forEach((video) => {
|
|
34
35
|
video.muted = value;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VideoPlayer.web.js","sourceRoot":"","sources":["../src/VideoPlayer.web.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useMemo } from 'react';\n\nimport { VideoPlayer, VideoPlayerEvents, VideoPlayerStatus, VideoSource } 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 useMemo(() => {\n const player = new VideoPlayerWeb(parsedSource);\n setup?.(player);\n return player;\n }, [JSON.stringify(source)]);\n}\n\nexport function getSourceUri(source: VideoSource): string | null {\n if (typeof source == 'string') {\n return source;\n }\n return source?.uri ?? null;\n}\n\nexport class VideoPlayerWeb\n extends globalThis.expo.SharedObject<VideoPlayerEvents>\n implements VideoPlayer\n{\n constructor(source: VideoSource) {\n super();\n this.src = source;\n }\n\n src: VideoSource = null;\n _mountedVideos: Set<HTMLVideoElement> = new Set();\n _audioNodes: Set<MediaElementAudioSourceNode> = 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: VideoPlayerStatus = '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\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\n set preservesPitch(value: boolean) {\n this._mountedVideos.forEach((video) => {\n video.preservesPitch = value;\n });\n this._preservesPitch = value;\n }\n\n get status(): VideoPlayerStatus {\n return this._status;\n }\n\n mountVideoView(video: HTMLVideoElement) {\n this._mountedVideos.add(video);\n this._addListeners(video);\n this._synchronizeWithFirstVideo(video);\n }\n\n unmountVideoView(video: HTMLVideoElement) {\n this._mountedVideos.delete(video);\n }\n\n mountAudioNode(\n audioContext: AudioContext,\n zeroGainNode: GainNode,\n audioSourceNode: MediaElementAudioSourceNode\n ): void {\n if (!audioContext || !zeroGainNode) return;\n\n this._audioNodes.add(audioSourceNode);\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 audioSourceNode.connect(audioContext.destination);\n } else {\n audioSourceNode.connect(zeroGainNode);\n }\n }\n\n unmountAudioNode(\n video: HTMLVideoElement,\n audioContext: AudioContext,\n audioSourceNode: MediaElementAudioSourceNode\n ) {\n const mountedVideos = [...this._mountedVideos];\n const videoPlayingAudio = mountedVideos[0];\n this._audioNodes.delete(audioSourceNode);\n audioSourceNode.disconnect();\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\n pause(): void {\n this._mountedVideos.forEach((video) => {\n video.pause();\n });\n this.playing = false;\n }\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\n seekBy(seconds: number): void {\n this._mountedVideos.forEach((video) => {\n video.currentTime += seconds;\n });\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\n if (firstVideo.paused) {\n video.pause();\n } else {\n video.play();\n }\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.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 if (this.playing && video.paused) {\n video.play();\n }\n };\n\n video.onwaiting = () => {\n this._status = 'loading';\n };\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"VideoPlayer.web.js","sourceRoot":"","sources":["../src/VideoPlayer.web.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useMemo } from 'react';\n\nimport { VideoPlayer, VideoPlayerEvents, VideoPlayerStatus, VideoSource } 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 useMemo(() => {\n const player = new VideoPlayerWeb(parsedSource);\n setup?.(player);\n return player;\n }, [JSON.stringify(source)]);\n}\n\nexport function getSourceUri(source: VideoSource): string | null {\n if (typeof source == 'string') {\n return source;\n }\n return source?.uri ?? null;\n}\n\nexport class VideoPlayerWeb\n extends globalThis.expo.SharedObject<VideoPlayerEvents>\n implements VideoPlayer\n{\n constructor(source: VideoSource) {\n super();\n this.src = source;\n }\n\n src: VideoSource = null;\n _mountedVideos: Set<HTMLVideoElement> = new Set();\n _audioNodes: Set<MediaElementAudioSourceNode> = 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: VideoPlayerStatus = 'idle';\n staysActiveInBackground: boolean = false; // Not supported on web. Dummy to match the interface.\n showNowPlayingNotification: 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\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\n set preservesPitch(value: boolean) {\n this._mountedVideos.forEach((video) => {\n video.preservesPitch = value;\n });\n this._preservesPitch = value;\n }\n\n get status(): VideoPlayerStatus {\n return this._status;\n }\n\n mountVideoView(video: HTMLVideoElement) {\n this._mountedVideos.add(video);\n this._addListeners(video);\n this._synchronizeWithFirstVideo(video);\n }\n\n unmountVideoView(video: HTMLVideoElement) {\n this._mountedVideos.delete(video);\n }\n\n mountAudioNode(\n audioContext: AudioContext,\n zeroGainNode: GainNode,\n audioSourceNode: MediaElementAudioSourceNode\n ): void {\n if (!audioContext || !zeroGainNode) return;\n\n this._audioNodes.add(audioSourceNode);\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 audioSourceNode.connect(audioContext.destination);\n } else {\n audioSourceNode.connect(zeroGainNode);\n }\n }\n\n unmountAudioNode(\n video: HTMLVideoElement,\n audioContext: AudioContext,\n audioSourceNode: MediaElementAudioSourceNode\n ) {\n const mountedVideos = [...this._mountedVideos];\n const videoPlayingAudio = mountedVideos[0];\n this._audioNodes.delete(audioSourceNode);\n audioSourceNode.disconnect();\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\n pause(): void {\n this._mountedVideos.forEach((video) => {\n video.pause();\n });\n this.playing = false;\n }\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\n seekBy(seconds: number): void {\n this._mountedVideos.forEach((video) => {\n video.currentTime += seconds;\n });\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\n if (firstVideo.paused) {\n video.pause();\n } else {\n video.play();\n }\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.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 if (this.playing && video.paused) {\n video.play();\n }\n };\n\n video.onwaiting = () => {\n this._status = 'loading';\n };\n }\n}\n"]}
|
|
@@ -48,6 +48,10 @@ export declare class VideoPlayer extends SharedObject<VideoPlayerEvents> {
|
|
|
48
48
|
* > This property is get-only
|
|
49
49
|
*/
|
|
50
50
|
status: VideoPlayerStatus;
|
|
51
|
+
/**
|
|
52
|
+
* Boolean value determining whether the player should show the now playing notification.
|
|
53
|
+
*/
|
|
54
|
+
showNowPlayingNotification: boolean;
|
|
51
55
|
/**
|
|
52
56
|
* Determines whether the player should continue playing after the app enters the background.
|
|
53
57
|
* @default false
|
|
@@ -163,7 +167,7 @@ export interface VideoViewProps extends ViewProps {
|
|
|
163
167
|
}
|
|
164
168
|
/**
|
|
165
169
|
* Specifies which type of DRM to use. Android supports Widevine, PlayReady and ClearKey, iOS supports FairPlay.
|
|
166
|
-
|
|
170
|
+
*/
|
|
167
171
|
export type DRMType = 'clearkey' | 'fairplay' | 'playready' | 'widevine';
|
|
168
172
|
/**
|
|
169
173
|
* Specifies DRM options which will be used by the player while loading the video.
|
|
@@ -200,8 +204,19 @@ export type DRMOptions = {
|
|
|
200
204
|
certificateUrl?: string;
|
|
201
205
|
};
|
|
202
206
|
export type VideoSource = string | {
|
|
207
|
+
/**
|
|
208
|
+
* The URI of the video.
|
|
209
|
+
*/
|
|
203
210
|
uri: string;
|
|
211
|
+
/**
|
|
212
|
+
* Specifies the DRM options which will be used by the player while loading the video.
|
|
213
|
+
*/
|
|
204
214
|
drm?: DRMOptions;
|
|
215
|
+
/**
|
|
216
|
+
* Specifies information which will be displayed in the now playing notification.
|
|
217
|
+
* When undefined the player will display information contained in the video metadata.
|
|
218
|
+
*/
|
|
219
|
+
metadata?: VideoMetadata;
|
|
205
220
|
} | null;
|
|
206
221
|
/**
|
|
207
222
|
* Handlers for events which can be emitted by the player.
|
|
@@ -245,4 +260,17 @@ export type VolumeEvent = {
|
|
|
245
260
|
volume: number;
|
|
246
261
|
isMuted: boolean;
|
|
247
262
|
};
|
|
263
|
+
/**
|
|
264
|
+
* Contains information that will be displayed in the now playing notification when the video is playing.
|
|
265
|
+
*/
|
|
266
|
+
export type VideoMetadata = {
|
|
267
|
+
/**
|
|
268
|
+
* The title of the video.
|
|
269
|
+
*/
|
|
270
|
+
title?: string;
|
|
271
|
+
/**
|
|
272
|
+
* Secondary text that will be displayed under the title.
|
|
273
|
+
*/
|
|
274
|
+
artist?: string;
|
|
275
|
+
};
|
|
248
276
|
//# sourceMappingURL=VideoView.types.d.ts.map
|
|
@@ -1 +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,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,iBAAiB,CAAC;IAE1B;;;;;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,MAAM,MAAM,eAAe,GAAG,SAAS,GAAG,OAAO,GAAG,MAAM,CAAC;AAE3D;;;;;;GAMG;AACH,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,SAAS,GAAG,aAAa,GAAG,OAAO,CAAC;AAE7E,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;;
|
|
1
|
+
{"version":3,"file":"VideoView.types.d.ts","sourceRoot":"","sources":["../src/VideoView.types.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +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<VideoPlayerEvents> {\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: VideoPlayerStatus;\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 */\nexport type 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 VideoPlayerStatus = '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
|
|
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<VideoPlayerEvents> {\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: VideoPlayerStatus;\n\n /**\n * Boolean value determining whether the player should show the now playing notification.\n */\n showNowPlayingNotification: boolean;\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 */\nexport type 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 VideoPlayerStatus = '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 */\nexport type DRMType = 'clearkey' | 'fairplay' | 'playready' | 'widevine';\n\n/**\n * Specifies DRM options which will be used by the player while loading the video.\n */\nexport type 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 =\n | string\n | {\n /**\n * The URI of the video.\n */\n uri: string;\n /**\n * Specifies the DRM options which will be used by the player while loading the video.\n */\n drm?: DRMOptions;\n /**\n * Specifies information which will be displayed in the now playing notification.\n * When undefined the player will display information contained in the video metadata.\n */\n metadata?: VideoMetadata;\n }\n | 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: (\n newStatus: VideoPlayerStatus,\n oldStatus: VideoPlayerStatus,\n error: PlayerError\n ) => 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 */\nexport type PlayerError = {\n message: string;\n};\n\n/**\n * Contains information about the current volume and whether the player is muted.\n */\nexport type VolumeEvent = {\n volume: number;\n isMuted: boolean;\n};\n\n/**\n * Contains information that will be displayed in the now playing notification when the video is playing.\n */\nexport type VideoMetadata = {\n /**\n * The title of the video.\n */\n title?: string;\n /**\n * Secondary text that will be displayed under the title.\n */\n artist?: string;\n};\n"]}
|
|
@@ -2,20 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
import Foundation
|
|
4
4
|
import MediaPlayer
|
|
5
|
+
import ExpoModulesCore
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* A class meant to manage the "NowPlaying" widget based on currently playing players. When multiple players
|
|
8
9
|
* are present the one that has most recently started playing and will be used as the source of information for the widget.
|
|
9
10
|
* Paused player will be used as a data source for "NowPlaying" only if no other players are currently playing.
|
|
10
11
|
*/
|
|
11
|
-
class NowPlayingManager {
|
|
12
|
+
class NowPlayingManager: VideoPlayerObserverDelegate {
|
|
12
13
|
static var shared = NowPlayingManager()
|
|
13
14
|
|
|
14
15
|
private let skipTimeInterval = 10.0
|
|
15
16
|
private var timeObserver: Any?
|
|
16
17
|
private weak var mostRecentInteractionPlayer: AVPlayer?
|
|
17
|
-
private var players = NSHashTable<
|
|
18
|
-
private var observations: [AVPlayer: NSKeyValueObservation] = [:]
|
|
18
|
+
private var players = NSHashTable<VideoPlayer>.weakObjects()
|
|
19
19
|
|
|
20
20
|
private var playTarget: Any?
|
|
21
21
|
private var pauseTarget: Any?
|
|
@@ -30,20 +30,23 @@ class NowPlayingManager {
|
|
|
30
30
|
commandCenter.skipBackwardCommand.preferredIntervals = [NSNumber(value: skipTimeInterval)]
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
func registerPlayer(_ player:
|
|
34
|
-
if let oldObservation = observations[player] {
|
|
35
|
-
oldObservation.invalidate()
|
|
36
|
-
}
|
|
37
|
-
observations[player] = observePlayerRate(player: player)
|
|
33
|
+
func registerPlayer(_ player: VideoPlayer) {
|
|
38
34
|
players.add(player)
|
|
39
|
-
|
|
35
|
+
player.observer.registerDelegate(delegate: self)
|
|
40
36
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
observation.invalidate()
|
|
37
|
+
if mostRecentInteractionPlayer == nil {
|
|
38
|
+
setMostRecentInteractionPlayer(player: player.pointer)
|
|
44
39
|
}
|
|
45
|
-
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
func unregisterPlayer(_ player: VideoPlayer) {
|
|
46
43
|
players.remove(player)
|
|
44
|
+
player.observer.unregisterDelegate(delegate: self)
|
|
45
|
+
|
|
46
|
+
if mostRecentInteractionPlayer == player.pointer {
|
|
47
|
+
let newPlayer = players.allObjects.first(where: { $0.playbackRate != 0 })
|
|
48
|
+
setMostRecentInteractionPlayer(player: newPlayer?.pointer)
|
|
49
|
+
}
|
|
47
50
|
|
|
48
51
|
if players.allObjects.isEmpty {
|
|
49
52
|
let commandCenter = MPRemoteCommandCenter.shared()
|
|
@@ -53,7 +56,7 @@ class NowPlayingManager {
|
|
|
53
56
|
}
|
|
54
57
|
}
|
|
55
58
|
|
|
56
|
-
private func setMostRecentInteractionPlayer(player: AVPlayer) {
|
|
59
|
+
private func setMostRecentInteractionPlayer(player: AVPlayer?) {
|
|
57
60
|
if player == mostRecentInteractionPlayer {
|
|
58
61
|
return
|
|
59
62
|
}
|
|
@@ -64,12 +67,13 @@ class NowPlayingManager {
|
|
|
64
67
|
|
|
65
68
|
self.mostRecentInteractionPlayer = player
|
|
66
69
|
self.setupNowPlayingControls()
|
|
70
|
+
self.updateNowPlayingInfo()
|
|
67
71
|
|
|
68
|
-
timeObserver = player
|
|
72
|
+
timeObserver = player?.addPeriodicTimeObserver(
|
|
69
73
|
forInterval: CMTimeMake(value: 1, timescale: 4),
|
|
70
74
|
queue: .main,
|
|
71
75
|
using: { [weak self] _ in
|
|
72
|
-
self?.
|
|
76
|
+
self?.updateNowPlayingDynamicValues()
|
|
73
77
|
})
|
|
74
78
|
}
|
|
75
79
|
|
|
@@ -95,7 +99,7 @@ class NowPlayingManager {
|
|
|
95
99
|
}
|
|
96
100
|
|
|
97
101
|
for player in players.allObjects {
|
|
98
|
-
player.pause()
|
|
102
|
+
player.pointer.pause()
|
|
99
103
|
}
|
|
100
104
|
return .success
|
|
101
105
|
}
|
|
@@ -137,26 +141,34 @@ class NowPlayingManager {
|
|
|
137
141
|
guard let player = mostRecentInteractionPlayer, let currentItem = mostRecentInteractionPlayer?.currentItem else {
|
|
138
142
|
return
|
|
139
143
|
}
|
|
140
|
-
let
|
|
144
|
+
let videoPlayerItem = currentItem as? VideoPlayerItem
|
|
145
|
+
|
|
146
|
+
// Metadata explicily specified by the user
|
|
147
|
+
let userMetadata = videoPlayerItem?.videoSource.metadata
|
|
148
|
+
|
|
149
|
+
// Metadata fetched with the video
|
|
150
|
+
let assetMetadata = currentItem.asset.commonMetadata
|
|
141
151
|
|
|
142
|
-
let title =
|
|
152
|
+
let title = assetMetadata.first(where: {
|
|
143
153
|
$0.commonKey == .commonKeyTitle
|
|
144
154
|
})
|
|
145
155
|
|
|
146
|
-
let artist =
|
|
156
|
+
let artist = assetMetadata.first(where: {
|
|
147
157
|
$0.commonKey == .commonKeyArtist
|
|
148
158
|
})
|
|
149
159
|
|
|
150
|
-
let artwork =
|
|
160
|
+
let artwork = assetMetadata.first(where: {
|
|
151
161
|
$0.commonKey == .commonKeyArtwork
|
|
152
162
|
})
|
|
153
163
|
|
|
154
|
-
var nowPlayingInfo = [
|
|
164
|
+
var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [:]
|
|
155
165
|
|
|
156
|
-
nowPlayingInfo[MPMediaItemPropertyTitle] = title
|
|
166
|
+
nowPlayingInfo[MPMediaItemPropertyTitle] = userMetadata?.title ?? title
|
|
167
|
+
nowPlayingInfo[MPMediaItemPropertyArtist] = userMetadata?.artist ?? artist
|
|
157
168
|
nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = currentItem.duration.seconds
|
|
158
169
|
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentItem.currentTime().seconds
|
|
159
170
|
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = player.rate
|
|
171
|
+
nowPlayingInfo[MPNowPlayingInfoPropertyMediaType] = MPNowPlayingInfoMediaType.video.rawValue // Using MPNowPlayingInfoMediaType.video causes a crash
|
|
160
172
|
nowPlayingInfo[MPMediaItemPropertyArtwork] = artwork
|
|
161
173
|
|
|
162
174
|
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
|
|
@@ -172,6 +184,7 @@ class NowPlayingManager {
|
|
|
172
184
|
return
|
|
173
185
|
}
|
|
174
186
|
|
|
187
|
+
nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = currentItem.duration.seconds
|
|
175
188
|
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentItem.currentTime().seconds.rounded()
|
|
176
189
|
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = player.rate
|
|
177
190
|
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
|
|
@@ -185,20 +198,26 @@ class NowPlayingManager {
|
|
|
185
198
|
commandCenter.changePlaybackPositionCommand.removeTarget(playbackPositionTarget)
|
|
186
199
|
}
|
|
187
200
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
}
|
|
201
|
+
func onItemChanged(player: AVPlayer, oldVideoPlayerItem: VideoPlayerItem?, newVideoPlayerItem: VideoPlayerItem?) {
|
|
202
|
+
setupNowPlayingControls()
|
|
203
|
+
updateNowPlayingInfo()
|
|
204
|
+
}
|
|
193
205
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
199
|
-
} else if newRate != 0 && mostRecentInteractionPlayer != changedPlayer {
|
|
200
|
-
setMostRecentInteractionPlayer(player: changedPlayer)
|
|
206
|
+
func onRateChanged(player: AVPlayer, oldRate: Float?, newRate: Float) {
|
|
207
|
+
if newRate == 0 && mostRecentInteractionPlayer == player {
|
|
208
|
+
if let newPlayer = players.allObjects.first(where: { $0.pointer.rate != 0 }) {
|
|
209
|
+
setMostRecentInteractionPlayer(player: newPlayer.pointer)
|
|
201
210
|
}
|
|
211
|
+
} else if newRate != 0 && mostRecentInteractionPlayer != player {
|
|
212
|
+
setMostRecentInteractionPlayer(player: player)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
func onPlayerItemStatusChanged(player: AVPlayer, oldStatus: AVPlayerItem.Status?, newStatus: AVPlayerItem.Status) {
|
|
217
|
+
// The player can be registered before it's item has loaded. We have to re-update the notification when item data is loaded
|
|
218
|
+
if player == mostRecentInteractionPlayer && newStatus == .readyToPlay {
|
|
219
|
+
setupNowPlayingControls()
|
|
220
|
+
updateNowPlayingInfo()
|
|
202
221
|
}
|
|
203
222
|
}
|
|
204
223
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Copyright 2024-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import ExpoModulesCore
|
|
4
|
+
|
|
5
|
+
// swiftlint:disable redundant_optional_initialization - Initialization with nil is necessary
|
|
6
|
+
internal struct VideoMetadata: Record {
|
|
7
|
+
@Field
|
|
8
|
+
var title: String? = nil
|
|
9
|
+
|
|
10
|
+
@Field
|
|
11
|
+
var artist: String? = nil
|
|
12
|
+
}
|
|
13
|
+
// swiftlint:enable redundant_optional_initialization
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
// Copyright 2024-present 650 Industries. All rights reserved.
|
|
2
2
|
|
|
3
|
-
import Foundation
|
|
4
3
|
import ExpoModulesCore
|
|
5
4
|
|
|
5
|
+
// swiftlint:disable redundant_optional_initialization - Initialization with nil is necessary
|
|
6
6
|
internal struct VideoSource: Record {
|
|
7
7
|
@Field
|
|
8
|
-
var uri: URL?
|
|
8
|
+
var uri: URL? = nil
|
|
9
|
+
|
|
10
|
+
@Field
|
|
11
|
+
var drm: DRMOptions? = nil
|
|
12
|
+
|
|
9
13
|
@Field
|
|
10
|
-
var
|
|
14
|
+
var metadata: VideoMetadata? = nil
|
|
11
15
|
}
|
|
16
|
+
// swiftlint:enable redundant_optional_initialization
|
package/ios/VideoManager.swift
CHANGED
|
@@ -50,17 +50,46 @@ class VideoManager {
|
|
|
50
50
|
|
|
51
51
|
// MARK: - Audio Session Management
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
internal func setAppropriateAudioSessionOrWarn() {
|
|
54
54
|
let audioSession = AVAudioSession.sharedInstance()
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
var audioSessionCategoryOptions: AVAudioSession.CategoryOptions = []
|
|
56
|
+
|
|
57
|
+
let isAnyPlayerPlaying = videoPlayers.allObjects.contains { player in
|
|
58
|
+
player.isPlaying
|
|
59
|
+
}
|
|
60
|
+
let areAllPlayersMuted = videoPlayers.allObjects.allSatisfy { player in
|
|
61
|
+
player.isMuted
|
|
62
|
+
}
|
|
63
|
+
let needsPiPSupport = videoViews.allObjects.contains { view in
|
|
64
|
+
view.allowPictureInPicture
|
|
65
|
+
}
|
|
66
|
+
let anyPlayerShowsNotification = videoPlayers.allObjects.contains { player in
|
|
67
|
+
player.showNowPlayingNotification
|
|
68
|
+
}
|
|
69
|
+
// The notification won't be shown if we allow the audio to mix with others
|
|
70
|
+
let shouldAllowMixing = (!isAnyPlayerPlaying || areAllPlayersMuted) && !anyPlayerShowsNotification
|
|
71
|
+
let isOutputtingAudio = !areAllPlayersMuted && isAnyPlayerPlaying
|
|
72
|
+
let shouldUpdateToAllowMixing = !audioSession.categoryOptions.contains(.mixWithOthers) && shouldAllowMixing
|
|
58
73
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
74
|
+
if shouldAllowMixing {
|
|
75
|
+
audioSessionCategoryOptions.insert(.mixWithOthers)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if isOutputtingAudio || needsPiPSupport || shouldUpdateToAllowMixing || anyPlayerShowsNotification {
|
|
79
|
+
do {
|
|
80
|
+
try audioSession.setCategory(.playback, mode: .moviePlayback)
|
|
81
|
+
} catch {
|
|
82
|
+
log.warn("Failed to set audio session category. This might cause issues with audio playback and Picture in Picture. \(error.localizedDescription)")
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Make sure audio session is active if any video is playing
|
|
87
|
+
if isAnyPlayerPlaying {
|
|
88
|
+
do {
|
|
89
|
+
try audioSession.setActive(true)
|
|
90
|
+
} catch {
|
|
91
|
+
log.warn("Failed to activate the audio session. This might cause issues with audio playback. \(error.localizedDescription)")
|
|
92
|
+
}
|
|
64
93
|
}
|
|
65
94
|
}
|
|
66
95
|
}
|
package/ios/VideoModule.swift
CHANGED
|
@@ -139,6 +139,13 @@ public final class VideoModule: Module {
|
|
|
139
139
|
player.preservesPitch = preservesPitch
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
Property("showNowPlayingNotification") { player -> Bool in
|
|
143
|
+
return player.showNowPlayingNotification
|
|
144
|
+
}
|
|
145
|
+
.set {(player, showNowPlayingNotification: Bool) in
|
|
146
|
+
player.showNowPlayingNotification = showNowPlayingNotification
|
|
147
|
+
}
|
|
148
|
+
|
|
142
149
|
Property("status") { player -> PlayerStatus in
|
|
143
150
|
return player.status
|
|
144
151
|
}
|
|
@@ -162,7 +169,7 @@ public final class VideoModule: Module {
|
|
|
162
169
|
var videoSource: VideoSource?
|
|
163
170
|
|
|
164
171
|
if source.is(String.self), let url: String = source.get() {
|
|
165
|
-
videoSource = VideoSource(uri:
|
|
172
|
+
videoSource = VideoSource(uri: URL(string: url))
|
|
166
173
|
} else if source.is(VideoSource.self) {
|
|
167
174
|
videoSource = source.get()
|
|
168
175
|
}
|
package/ios/VideoPlayer.swift
CHANGED
|
@@ -6,7 +6,7 @@ import ExpoModulesCore
|
|
|
6
6
|
|
|
7
7
|
internal final class VideoPlayer: SharedRef<AVPlayer>, Hashable, VideoPlayerObserverDelegate {
|
|
8
8
|
lazy var contentKeyManager = ContentKeyManager()
|
|
9
|
-
|
|
9
|
+
let observer: VideoPlayerObserver
|
|
10
10
|
|
|
11
11
|
var loop = false
|
|
12
12
|
private(set) var isPlaying = false
|
|
@@ -26,9 +26,7 @@ internal final class VideoPlayer: SharedRef<AVPlayer>, Hashable, VideoPlayerObse
|
|
|
26
26
|
var staysActiveInBackground = false {
|
|
27
27
|
didSet {
|
|
28
28
|
if staysActiveInBackground {
|
|
29
|
-
VideoManager.shared.
|
|
30
|
-
warning: "Failed to set the audio session category. This might affect background playback functionality"
|
|
31
|
-
)
|
|
29
|
+
VideoManager.shared.setAppropriateAudioSessionOrWarn()
|
|
32
30
|
}
|
|
33
31
|
}
|
|
34
32
|
}
|
|
@@ -60,19 +58,34 @@ internal final class VideoPlayer: SharedRef<AVPlayer>, Hashable, VideoPlayerObse
|
|
|
60
58
|
self.emit(event: "volumeChange", arguments: newVolumeEvent.isMuted, oldVolumeEvent.isMuted)
|
|
61
59
|
}
|
|
62
60
|
pointer.isMuted = isMuted
|
|
61
|
+
VideoManager.shared.setAppropriateAudioSessionOrWarn()
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
var showNowPlayingNotification = false {
|
|
66
|
+
didSet {
|
|
67
|
+
// The audio session needs to be appropriate before displaying the notfication
|
|
68
|
+
VideoManager.shared.setAppropriateAudioSessionOrWarn()
|
|
69
|
+
|
|
70
|
+
if showNowPlayingNotification {
|
|
71
|
+
NowPlayingManager.shared.registerPlayer(self)
|
|
72
|
+
} else {
|
|
73
|
+
NowPlayingManager.shared.unregisterPlayer(self)
|
|
74
|
+
}
|
|
63
75
|
}
|
|
64
76
|
}
|
|
65
77
|
|
|
66
78
|
override init(_ pointer: AVPlayer) {
|
|
79
|
+
observer = VideoPlayerObserver(player: pointer)
|
|
67
80
|
super.init(pointer)
|
|
68
|
-
observer
|
|
69
|
-
NowPlayingManager.shared.registerPlayer(pointer)
|
|
81
|
+
observer.registerDelegate(delegate: self)
|
|
70
82
|
VideoManager.shared.register(videoPlayer: self)
|
|
71
83
|
}
|
|
72
84
|
|
|
73
85
|
deinit {
|
|
74
|
-
NowPlayingManager.shared.unregisterPlayer(
|
|
86
|
+
NowPlayingManager.shared.unregisterPlayer(self)
|
|
75
87
|
VideoManager.shared.unregister(videoPlayer: self)
|
|
88
|
+
observer.unregisterDelegate(delegate: self)
|
|
76
89
|
pointer.replaceCurrentItem(with: nil)
|
|
77
90
|
}
|
|
78
91
|
|
|
@@ -125,6 +138,8 @@ internal final class VideoPlayer: SharedRef<AVPlayer>, Hashable, VideoPlayerObse
|
|
|
125
138
|
func onIsPlayingChanged(player: AVPlayer, oldIsPlaying: Bool?, newIsPlaying: Bool) {
|
|
126
139
|
self.emit(event: "playingChange", arguments: newIsPlaying, oldIsPlaying)
|
|
127
140
|
isPlaying = newIsPlaying
|
|
141
|
+
|
|
142
|
+
VideoManager.shared.setAppropriateAudioSessionOrWarn()
|
|
128
143
|
}
|
|
129
144
|
|
|
130
145
|
func onRateChanged(player: AVPlayer, oldRate: Float?, newRate: Float) {
|
|
@@ -4,6 +4,14 @@ import Foundation
|
|
|
4
4
|
import ExpoModulesCore
|
|
5
5
|
import AVFoundation
|
|
6
6
|
|
|
7
|
+
private struct Weak<T: AnyObject> {
|
|
8
|
+
weak var value: T?
|
|
9
|
+
|
|
10
|
+
init(_ value: T?) {
|
|
11
|
+
self.value = value
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
7
15
|
protocol VideoPlayerObserverDelegate: AnyObject {
|
|
8
16
|
func onStatusChanged(player: AVPlayer, oldStatus: PlayerStatus?, newStatus: PlayerStatus, error: Exception?)
|
|
9
17
|
func onIsPlayingChanged(player: AVPlayer, oldIsPlaying: Bool?, newIsPlaying: Bool)
|
|
@@ -12,17 +20,55 @@ protocol VideoPlayerObserverDelegate: AnyObject {
|
|
|
12
20
|
func onPlayedToEnd(player: AVPlayer)
|
|
13
21
|
func onItemChanged(player: AVPlayer, oldVideoPlayerItem: VideoPlayerItem?, newVideoPlayerItem: VideoPlayerItem?)
|
|
14
22
|
func onIsMutedChanged(player: AVPlayer, oldIsMuted: Bool?, newIsMuted: Bool)
|
|
23
|
+
func onPlayerItemStatusChanged(player: AVPlayer, oldStatus: AVPlayerItem.Status?, newStatus: AVPlayerItem.Status)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Default implementations for the delegate
|
|
27
|
+
extension VideoPlayerObserverDelegate {
|
|
28
|
+
func onStatusChanged(player: AVPlayer, oldStatus: PlayerStatus?, newStatus: PlayerStatus, error: Exception?) {}
|
|
29
|
+
func onIsPlayingChanged(player: AVPlayer, oldIsPlaying: Bool?, newIsPlaying: Bool) {}
|
|
30
|
+
func onRateChanged(player: AVPlayer, oldRate: Float?, newRate: Float) {}
|
|
31
|
+
func onVolumeChanged(player: AVPlayer, oldVolume: Float?, newVolume: Float) {}
|
|
32
|
+
func onPlayedToEnd(player: AVPlayer) {}
|
|
33
|
+
func onItemChanged(player: AVPlayer, oldVideoPlayerItem: VideoPlayerItem?, newVideoPlayerItem: VideoPlayerItem?) {}
|
|
34
|
+
func onIsMutedChanged(player: AVPlayer, oldIsMuted: Bool?, newIsMuted: Bool) {}
|
|
35
|
+
func onPlayerItemStatusChanged(player: AVPlayer, oldStatus: AVPlayerItem.Status?, newStatus: AVPlayerItem.Status) {}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Wrapper used to store WeakReferences to the observer delegate
|
|
39
|
+
final class WeakPlayerObserverDelegate: Hashable {
|
|
40
|
+
private(set) weak var value: VideoPlayerObserverDelegate?
|
|
41
|
+
|
|
42
|
+
init(value: VideoPlayerObserverDelegate? = nil) {
|
|
43
|
+
self.value = value
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
static func == (lhs: WeakPlayerObserverDelegate, rhs: WeakPlayerObserverDelegate) -> Bool {
|
|
47
|
+
guard let lhsValue = lhs.value, let rhsValue = rhs.value else {
|
|
48
|
+
return lhs.value == nil && rhs.value == nil
|
|
49
|
+
}
|
|
50
|
+
return ObjectIdentifier(lhsValue) == ObjectIdentifier(rhsValue)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
func hash(into hasher: inout Hasher) {
|
|
54
|
+
if let value {
|
|
55
|
+
hasher.combine(ObjectIdentifier(value))
|
|
56
|
+
}
|
|
57
|
+
}
|
|
15
58
|
}
|
|
16
59
|
|
|
17
60
|
class VideoPlayerObserver {
|
|
18
61
|
let player: AVPlayer
|
|
62
|
+
var delegates = Set<WeakPlayerObserverDelegate>()
|
|
19
63
|
weak var delegate: VideoPlayerObserverDelegate?
|
|
20
64
|
private var currentItem: VideoPlayerItem?
|
|
21
65
|
|
|
22
66
|
private var isPlaying: Bool = false {
|
|
23
67
|
didSet {
|
|
24
68
|
if oldValue != isPlaying {
|
|
25
|
-
|
|
69
|
+
delegates.forEach { delegate in
|
|
70
|
+
delegate.value?.onIsPlayingChanged(player: player, oldIsPlaying: oldValue, newIsPlaying: isPlaying)
|
|
71
|
+
}
|
|
26
72
|
}
|
|
27
73
|
}
|
|
28
74
|
}
|
|
@@ -30,7 +76,9 @@ class VideoPlayerObserver {
|
|
|
30
76
|
private var status: PlayerStatus = .idle {
|
|
31
77
|
didSet {
|
|
32
78
|
if oldValue != status {
|
|
33
|
-
|
|
79
|
+
delegates.forEach { delegate in
|
|
80
|
+
delegate.value?.onStatusChanged(player: player, oldStatus: oldValue, newStatus: status, error: error)
|
|
81
|
+
}
|
|
34
82
|
}
|
|
35
83
|
}
|
|
36
84
|
}
|
|
@@ -50,9 +98,8 @@ class VideoPlayerObserver {
|
|
|
50
98
|
private var playerItemStatusObserver: NSKeyValueObservation?
|
|
51
99
|
private var playbackLikelyToKeepUpObserver: NSKeyValueObservation?
|
|
52
100
|
|
|
53
|
-
init(player: AVPlayer
|
|
101
|
+
init(player: AVPlayer) {
|
|
54
102
|
self.player = player
|
|
55
|
-
self.delegate = delegate
|
|
56
103
|
initializePlayerObservers()
|
|
57
104
|
}
|
|
58
105
|
|
|
@@ -61,6 +108,15 @@ class VideoPlayerObserver {
|
|
|
61
108
|
invalidateCurrentPlayerItemObservers()
|
|
62
109
|
}
|
|
63
110
|
|
|
111
|
+
func registerDelegate(delegate: VideoPlayerObserverDelegate) {
|
|
112
|
+
let weakDelegate = WeakPlayerObserverDelegate(value: delegate)
|
|
113
|
+
delegates.insert(weakDelegate)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
func unregisterDelegate(delegate: VideoPlayerObserverDelegate) {
|
|
117
|
+
delegates.remove(WeakPlayerObserverDelegate(value: delegate))
|
|
118
|
+
}
|
|
119
|
+
|
|
64
120
|
private func initializePlayerObservers() {
|
|
65
121
|
playerRateObserver = player.observe(\.rate, options: [.initial, .new, .old], changeHandler: onPlayerRateChanged)
|
|
66
122
|
playerStatusObserver = player.observe(\.status, options: [.initial, .new, .old], changeHandler: onPlayerStatusChanged)
|
|
@@ -80,7 +136,6 @@ class VideoPlayerObserver {
|
|
|
80
136
|
}
|
|
81
137
|
|
|
82
138
|
private func initializeCurrentPlayerItemObservers(player: AVPlayer, playerItem: AVPlayerItem) {
|
|
83
|
-
// Dual question marks due to the change wrapping an optional value as an optional Optional<Optional<PlayerItem>>
|
|
84
139
|
playbackBufferEmptyObserver = playerItem.observe(\.isPlaybackBufferEmpty, changeHandler: onIsBufferEmptyChanged)
|
|
85
140
|
playbackLikelyToKeepUpObserver = playerItem.observe(\.isPlaybackLikelyToKeepUp, changeHandler: onPlayerLikelyToKeepUpChanged)
|
|
86
141
|
playerItemStatusObserver = playerItem.observe(\.status, options: [.initial, .new], changeHandler: onItemStatusChanged)
|
|
@@ -90,7 +145,9 @@ class VideoPlayerObserver {
|
|
|
90
145
|
object: playerItem,
|
|
91
146
|
queue: nil
|
|
92
147
|
) { [weak self] _ in
|
|
93
|
-
self?.delegate
|
|
148
|
+
self?.delegates.forEach { delegate in
|
|
149
|
+
delegate.value?.onPlayedToEnd(player: player)
|
|
150
|
+
}
|
|
94
151
|
}
|
|
95
152
|
}
|
|
96
153
|
|
|
@@ -110,13 +167,18 @@ class VideoPlayerObserver {
|
|
|
110
167
|
|
|
111
168
|
if let videoPlayerItem = newPlayerItem as? VideoPlayerItem {
|
|
112
169
|
initializeCurrentPlayerItemObservers(player: player, playerItem: videoPlayerItem)
|
|
113
|
-
delegate?.onItemChanged(player: player, oldVideoPlayerItem: currentItem, newVideoPlayerItem: videoPlayerItem)
|
|
114
170
|
currentItem = videoPlayerItem
|
|
171
|
+
|
|
172
|
+
delegates.forEach { delegate in
|
|
173
|
+
delegate.value?.onItemChanged(player: player, oldVideoPlayerItem: currentItem, newVideoPlayerItem: videoPlayerItem)
|
|
174
|
+
}
|
|
115
175
|
return
|
|
116
176
|
}
|
|
117
177
|
|
|
118
178
|
if newPlayerItem == nil {
|
|
119
|
-
|
|
179
|
+
delegates.forEach { delegate in
|
|
180
|
+
delegate.value?.onItemChanged(player: player, oldVideoPlayerItem: currentItem, newVideoPlayerItem: nil)
|
|
181
|
+
}
|
|
120
182
|
status = .idle
|
|
121
183
|
} else {
|
|
122
184
|
log.warn(
|
|
@@ -145,6 +207,10 @@ class VideoPlayerObserver {
|
|
|
145
207
|
status = .readyToPlay
|
|
146
208
|
}
|
|
147
209
|
}
|
|
210
|
+
|
|
211
|
+
delegates.forEach { delegate in
|
|
212
|
+
delegate.value?.onPlayerItemStatusChanged(player: player, oldStatus: change.oldValue, newStatus: playerItem.status)
|
|
213
|
+
}
|
|
148
214
|
}
|
|
149
215
|
|
|
150
216
|
private func onPlayerStatusChanged(_ player: AVPlayer, _ change: NSKeyValueObservedChange<AVPlayer.Status>) {
|
|
@@ -193,19 +259,25 @@ class VideoPlayerObserver {
|
|
|
193
259
|
|
|
194
260
|
private func onPlayerRateChanged(_ player: AVPlayer, _ change: NSKeyValueObservedChange<Float>) {
|
|
195
261
|
if let newRate = change.newValue, change.oldValue != change.newValue {
|
|
196
|
-
|
|
262
|
+
delegates.forEach { delegate in
|
|
263
|
+
delegate.value?.onRateChanged(player: player, oldRate: change.oldValue, newRate: newRate)
|
|
264
|
+
}
|
|
197
265
|
}
|
|
198
266
|
}
|
|
199
267
|
|
|
200
268
|
private func onPlayerVolumeChanged(_ player: AVPlayer, _ change: NSKeyValueObservedChange<Float>) {
|
|
201
269
|
if let newVolume = change.newValue, change.oldValue != change.newValue {
|
|
202
|
-
|
|
270
|
+
delegates.forEach { delegate in
|
|
271
|
+
delegate.value?.onVolumeChanged(player: player, oldVolume: change.oldValue, newVolume: newVolume)
|
|
272
|
+
}
|
|
203
273
|
}
|
|
204
274
|
}
|
|
205
275
|
|
|
206
276
|
private func onPlayerIsMutedChanged(_ player: AVPlayer, _ change: NSKeyValueObservedChange<Bool>) {
|
|
207
277
|
if let newIsMuted = change.newValue, change.oldValue != change.newValue {
|
|
208
|
-
|
|
278
|
+
delegates.forEach { delegate in
|
|
279
|
+
delegate.value?.onIsMutedChanged(player: player, oldIsMuted: change.oldValue, newIsMuted: newIsMuted)
|
|
280
|
+
}
|
|
209
281
|
}
|
|
210
282
|
}
|
|
211
283
|
}
|
package/ios/VideoView.swift
CHANGED
|
@@ -6,7 +6,7 @@ import ExpoModulesCore
|
|
|
6
6
|
public final class VideoView: ExpoView, AVPlayerViewControllerDelegate {
|
|
7
7
|
lazy var playerViewController = AVPlayerViewController()
|
|
8
8
|
|
|
9
|
-
var player: VideoPlayer? {
|
|
9
|
+
weak var player: VideoPlayer? {
|
|
10
10
|
didSet {
|
|
11
11
|
playerViewController.player = player?.pointer
|
|
12
12
|
}
|
|
@@ -24,13 +24,8 @@ public final class VideoView: ExpoView, AVPlayerViewControllerDelegate {
|
|
|
24
24
|
|
|
25
25
|
var allowPictureInPicture: Bool = false {
|
|
26
26
|
didSet {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
// controls shows up automatically only when a correct audioSession category is set.
|
|
30
|
-
VideoManager.shared.switchToActiveAudioSessionOrWarn(
|
|
31
|
-
warning: "Failed to set the audio session category. This might break Picture in Picture functionality"
|
|
32
|
-
)
|
|
33
|
-
}
|
|
27
|
+
// PiP requires `.playback` audio session category in `.moviePlayback` mode
|
|
28
|
+
VideoManager.shared.setAppropriateAudioSessionOrWarn()
|
|
34
29
|
playerViewController.allowsPictureInPicturePlayback = allowPictureInPicture
|
|
35
30
|
}
|
|
36
31
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-video",
|
|
3
3
|
"title": "Expo Video",
|
|
4
|
-
"version": "1.1.
|
|
4
|
+
"version": "1.1.5",
|
|
5
5
|
"description": "A cross-platform, performant video component for React Native and Expo with Web support",
|
|
6
6
|
"main": "build/index.js",
|
|
7
7
|
"types": "build/index.d.ts",
|
|
@@ -36,5 +36,5 @@
|
|
|
36
36
|
"peerDependencies": {
|
|
37
37
|
"expo": "*"
|
|
38
38
|
},
|
|
39
|
-
"gitHead": "
|
|
39
|
+
"gitHead": "20f1765146f63f8fdc2543e8aba8b0e5140d1e05"
|
|
40
40
|
}
|
package/src/VideoPlayer.web.tsx
CHANGED
|
@@ -42,6 +42,7 @@ export class VideoPlayerWeb
|
|
|
42
42
|
_preservesPitch: boolean = true;
|
|
43
43
|
_status: VideoPlayerStatus = 'idle';
|
|
44
44
|
staysActiveInBackground: boolean = false; // Not supported on web. Dummy to match the interface.
|
|
45
|
+
showNowPlayingNotification: boolean = false; // Not supported on web. Dummy to match the interface.
|
|
45
46
|
|
|
46
47
|
set muted(value: boolean) {
|
|
47
48
|
this._mountedVideos.forEach((video) => {
|
package/src/VideoView.types.ts
CHANGED
|
@@ -57,6 +57,11 @@ export declare class VideoPlayer extends SharedObject<VideoPlayerEvents> {
|
|
|
57
57
|
*/
|
|
58
58
|
status: VideoPlayerStatus;
|
|
59
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Boolean value determining whether the player should show the now playing notification.
|
|
62
|
+
*/
|
|
63
|
+
showNowPlayingNotification: boolean;
|
|
64
|
+
|
|
60
65
|
/**
|
|
61
66
|
* Determines whether the player should continue playing after the app enters the background.
|
|
62
67
|
* @default false
|
|
@@ -188,7 +193,7 @@ export interface VideoViewProps extends ViewProps {
|
|
|
188
193
|
|
|
189
194
|
/**
|
|
190
195
|
* Specifies which type of DRM to use. Android supports Widevine, PlayReady and ClearKey, iOS supports FairPlay.
|
|
191
|
-
|
|
196
|
+
*/
|
|
192
197
|
export type DRMType = 'clearkey' | 'fairplay' | 'playready' | 'widevine';
|
|
193
198
|
|
|
194
199
|
/**
|
|
@@ -229,7 +234,24 @@ export type DRMOptions = {
|
|
|
229
234
|
certificateUrl?: string;
|
|
230
235
|
};
|
|
231
236
|
|
|
232
|
-
export type VideoSource =
|
|
237
|
+
export type VideoSource =
|
|
238
|
+
| string
|
|
239
|
+
| {
|
|
240
|
+
/**
|
|
241
|
+
* The URI of the video.
|
|
242
|
+
*/
|
|
243
|
+
uri: string;
|
|
244
|
+
/**
|
|
245
|
+
* Specifies the DRM options which will be used by the player while loading the video.
|
|
246
|
+
*/
|
|
247
|
+
drm?: DRMOptions;
|
|
248
|
+
/**
|
|
249
|
+
* Specifies information which will be displayed in the now playing notification.
|
|
250
|
+
* When undefined the player will display information contained in the video metadata.
|
|
251
|
+
*/
|
|
252
|
+
metadata?: VideoMetadata;
|
|
253
|
+
}
|
|
254
|
+
| null;
|
|
233
255
|
|
|
234
256
|
/**
|
|
235
257
|
* Handlers for events which can be emitted by the player.
|
|
@@ -279,3 +301,17 @@ export type VolumeEvent = {
|
|
|
279
301
|
volume: number;
|
|
280
302
|
isMuted: boolean;
|
|
281
303
|
};
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Contains information that will be displayed in the now playing notification when the video is playing.
|
|
307
|
+
*/
|
|
308
|
+
export type VideoMetadata = {
|
|
309
|
+
/**
|
|
310
|
+
* The title of the video.
|
|
311
|
+
*/
|
|
312
|
+
title?: string;
|
|
313
|
+
/**
|
|
314
|
+
* Secondary text that will be displayed under the title.
|
|
315
|
+
*/
|
|
316
|
+
artist?: string;
|
|
317
|
+
};
|