diff --git a/docs/pages/other/plugin.md b/docs/pages/other/plugin.md index 68bcc6e9..1ae8ddc8 100644 --- a/docs/pages/other/plugin.md +++ b/docs/pages/other/plugin.md @@ -209,29 +209,80 @@ class MyAVPlayerAnalyticsPlugin: RNVAVPlayerPlugin { override func onInstanceRemoved(id: String, player: AVPlayer) { // Handle AVPlayer removal with type-safe access } + + /// Optionally override the asset used by the player before playback starts + override func overridePlayerAsset(source: VideoSource, asset: AVAsset) async -> OverridePlayerAssetResult? { + // Return a modified asset or nil to use the default + return nil + } } ``` -The `RNVPlugin` class defines two methods: +The `RNVAVPlayerPlugin` class defines several extension points: ```swift /** - * Function called when a new player is created + * Function called when a new AVPlayer instance is created * @param id: a random string identifying the player - * @param player: the instantiated player reference + * @param player: the instantiated AVPlayer */ -open func onInstanceCreated(id: String, player: Any) { /* no-op */ } +open func onInstanceCreated(id: String, player: AVPlayer) { /* no-op */ } /** - * Function called when a player should be destroyed - * when this callback is called, the plugin shall free all - * resources and release all reference to Player object + * Function called when an AVPlayer instance is being removed * @param id: a random string identifying the player - * @param player: the player to release + * @param player: the AVPlayer to release */ -open func onInstanceRemoved(id: String, player: Any) { /* no-op */ } +open func onInstanceRemoved(id: String, player: AVPlayer) { /* no-op */ } + +/** + * Optionally override the asset used by the player before playback starts. + * Allows you to modify or replace the AVAsset before it is used to create the AVPlayerItem. + * Return nil to use the default asset. + * + * @param source: The VideoSource describing the video (uri, type, headers, etc.) + * @param asset: The AVAsset prepared by the player + * @return: OverridePlayerAssetResult if you want to override, or nil to use the default + */ +open func overridePlayerAsset(source: VideoSource, asset: AVAsset) async -> OverridePlayerAssetResult? { nil } ``` +##### `OverridePlayerAssetResult` and `OverridePlayerAssetType` + +To override the asset, return an `OverridePlayerAssetResult`: + +```swift +public struct OverridePlayerAssetResult { + public let type: OverridePlayerAssetType + public let asset: AVAsset + + public init(type: OverridePlayerAssetType, asset: AVAsset) { + self.type = type + self.asset = asset + } +} + +public enum OverridePlayerAssetType { + case partial // Return a partially modified asset; will go through the default prepare process + case full // Return a fully modified asset; will skip the default prepare process +} +``` + +- Use `.partial` if you want the asset to continue through the player's normal preparation (e.g., for text tracks or metadata injection). +- Use `.full` if you want to provide a fully prepared asset that will be used as-is for playback. + +**Example:** + +```swift +override func overridePlayerAsset(source: VideoSource, asset: AVAsset) async -> OverridePlayerAssetResult? { + // Example: Replace the asset URL + let newAsset = AVAsset(url: URL(string: "https://example.com/override.mp4")!) + return Result(type: .full, asset: newAsset) +} +``` + +> Only one plugin can override the player asset at a time. If multiple plugins implement this, only the first will be used. + ### 3. Register the Plugin To register the plugin in `react-native-video`, call: diff --git a/examples/expo/ios/Podfile.lock b/examples/expo/ios/Podfile.lock index 0b4e0fa2..29470433 100644 --- a/examples/expo/ios/Podfile.lock +++ b/examples/expo/ios/Podfile.lock @@ -1,19 +1,19 @@ PODS: - boost (1.84.0) - DoubleConversion (1.1.6) - - EXConstants (17.0.3): + - EXConstants (17.0.8): - ExpoModulesCore - - Expo (52.0.7): + - Expo (52.0.46): - ExpoModulesCore - - ExpoAsset (11.0.1): + - ExpoAsset (11.0.5): - ExpoModulesCore - - ExpoFileSystem (18.0.3): + - ExpoFileSystem (18.0.12): - ExpoModulesCore - - ExpoFont (13.0.1): + - ExpoFont (13.0.4): - ExpoModulesCore - - ExpoKeepAwake (14.0.1): + - ExpoKeepAwake (14.0.3): - ExpoModulesCore - - ExpoModulesCore (2.0.3): + - ExpoModulesCore (2.2.3): - DoubleConversion - glog - hermes-engine @@ -36,7 +36,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - ExpoSplashScreen (0.29.11): + - ExpoSplashScreen (0.29.24): - ExpoModulesCore - FBLazyVector (0.76.2-0) - fmt (9.1.0) @@ -1278,7 +1278,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - react-native-video (6.10.0): + - react-native-video (6.12.0): - DoubleConversion - glog - hermes-engine @@ -1291,7 +1291,7 @@ PODS: - React-featureflags - React-graphics - React-ImageManager - - react-native-video/Video (= 6.10.0) + - react-native-video/Video (= 6.12.0) - React-NativeModulesApple - React-RCTFabric - React-rendererdebug @@ -1322,7 +1322,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - react-native-video/Video (6.10.0): + - react-native-video/Video (6.12.0): - DoubleConversion - glog - hermes-engine @@ -1851,80 +1851,80 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: ad4d3a971e4f0b520a127c02c8c3f98b43394c15 DoubleConversion: 9a8708fb8299350a38a3425761aaea31263b7443 - EXConstants: dd2fe64c6cdb1383b694c309a63028a8e9f2be6d - Expo: 46cbe74ce0d0f4a4d7b726e90693eb8dfcec6de0 - ExpoAsset: 8138f2a9ec55ae1ad7c3871448379f7d97692d15 - ExpoFileSystem: cc31b7a48031ab565f9eb5c2b61aa08d774a271a - ExpoFont: 7522d869d84ee2ee8093ee997fef5b86f85d856b - ExpoKeepAwake: 783e68647b969b210a786047c3daa7b753dcac1f - ExpoModulesCore: 2d1df04dc27f91d8b383c0ec7f1d121e3d9b7f68 - ExpoSplashScreen: 26cee50e9e95572baf87cd3a8ccaf2ffc3856422 + EXConstants: fcfc75800824ac2d5c592b5bc74130bad17b146b + Expo: a6ff273c618506b12129a0d06f2a08201bfbcf43 + ExpoAsset: 48386d40d53a8c1738929b3ed509bcad595b5516 + ExpoFileSystem: 42d363d3b96f9afab980dcef60d5657a4443c655 + ExpoFont: f354e926f8feae5e831ec8087f36652b44a0b188 + ExpoKeepAwake: b0171a73665bfcefcfcc311742a72a956e6aa680 + ExpoModulesCore: 15f60d00e33ca0cea27147543877ca6e70c42ef5 + ExpoSplashScreen: 399ee9f85b6c8a61b965e13a1ecff8384db591c2 FBLazyVector: 6bbb8b88508d32cdf0ffb7ceddedf6aca79eff14 fmt: a150d37a5595336f19c91e6f1db7fcf17d5eb99f glog: 9ab333e271c177cecb6362fe27832c88667cb7e9 hermes-engine: 7bc33a9a81e6a58b3bc3426105ecd59981ac730e - RCT-Folly: 2512380c804686400e7f60b9e8b60525df4a6191 + RCT-Folly: 839d900d8e4d2b726f3277dd085d97c8904f9eac RCTDeprecation: b90bc991c207fa777f3d4f0d7f36d7473e62319a RCTRequired: c8fda281396e4e5cf96e3ab216c05986fb37ca3e RCTTypeSafety: 28a29fb79051f043943135d2e4403e77d3b7ec21 React: dade949d9edcee964714fe90a2892beef3d3a54b React-callinvoker: a6517e487e1645d2bc627be8d86b1f8dd5278bb5 - React-Core: 7010fbd0ed122476aa2cf7aa9619ec9ffb73c551 - React-CoreModules: 64eea3d14266e79f8362da71623a5157a7a6c41f - React-cxxreact: 01f36b93aa3c923f7607c50a43cc2b5a06129f07 + React-Core: 3b252d6ed5c2b9e50ed245f95aa4de1f6b5849ab + React-CoreModules: 5df4a1fa73a93ca46c33f22148b4f4693b64364e + React-cxxreact: e8b2b06dfbb9d646d9b45a29c3fc67f230e76fa9 React-debug: 25139c51b9fda09bb1745952c81cea9f46225d6a - React-defaultsnativemodule: 3805189f0d7bce05b3650d364676cf9b49e75eab - React-domnativemodule: abb803da2e62c1acbae61d277fd044fe77f6a9af - React-Fabric: c8d1c4207248fbaf9c046d92d81bab67d4694122 - React-FabricComponents: 7147628a2fec488771369e0a0dead89ad9649e0e - React-FabricImage: f814221b18a7d560baaa5fb807895c4c345036be + React-defaultsnativemodule: 6a217b48ce4eb487e7f0a9697ba6513a274cdbdc + React-domnativemodule: 64d0ffb108dc00a1275338afdfb78766fbd799d0 + React-Fabric: cb88e6fe3e97be05b425413dbba4fb6cab8e9b34 + React-FabricComponents: c5a6e9c81f9a64521ac8abf9ca2f2532e1398b86 + React-FabricImage: 724ae12ceb53b5f01dfc03d0acd63e392487713f React-featureflags: b684bbc804b9c85754fd009167a527a6eadbd9d5 - React-featureflagsnativemodule: 8b29cc7a659d17d0072924a83de9cf9a62a4d147 - React-graphics: 4d7d1fa0ea456c398de7b1f40f708885bccec2e8 - React-hermes: 981bd8b0f89a50a8fd06079bbdc4b11003135021 - React-idlecallbacksnativemodule: 12e7223f085d9d0b82aa0a10b0691294b11c792e - React-ImageManager: 4300a7a83ee603351659dade2e61e673ae3b88d2 - React-jserrorhandler: 40984dfabb1ab0b2e21d9d21af66530d5309aad0 - React-jsi: ecb93733e8efcd240957abf32b33624ce207d1f9 - React-jsiexecutor: 7ceddf35714b4c4a95ea1f590a16d96b1d577c49 - React-jsinspector: fe8b01493fb3cb8c6b946cb24c3e83e5d774fb1e - React-jsitracing: 47a37e9a623aadd867574d8143fa314df57f1f49 - React-logger: e928ead07d07fbe16f5bebc7ee004bfb7bf731d5 - React-Mapbuffer: 08b067fc26d19868b0a1fea3586d92afff4e8e4f - React-microtasksnativemodule: 309d3c6653f4ffcb8a3956604e07ab19b006ef82 - react-native-video: 25ecb15d6ac0f97e2d863cbe317c70bd6e8f1a6e - react-native-video-plugin-sample: 1f6b1dda047cdf07f6cd7e730dc92c1349656b1a + React-featureflagsnativemodule: 46d989a753848048218ea29a30c081274c42e661 + React-graphics: accc162849d86b3a8607ce80dd49afc129f35f66 + React-hermes: 2702dda9e6550e7bfcd6d11ed2dd5ac79ee824a7 + React-idlecallbacksnativemodule: c271aad98748669cf8a045b0559a0d3fd67013d2 + React-ImageManager: 6eb6ea557f32850680896907964fc0a8515721db + React-jserrorhandler: 6236637935a035413e9ada45c7a08a2a58fa7212 + React-jsi: a18137983c0690f3e29882128aa7daf624d2fdac + React-jsiexecutor: 75909d0eeac14133f33a0446b776388591e1c7c7 + React-jsinspector: db3334448e910b60b59cd1809dcf747970525077 + React-jsitracing: 545fc999f9db67b157323f439ff964caccae2c48 + React-logger: c1804a09cb1c3486fcd9c2b4753ba7b5b9ae1c38 + React-Mapbuffer: 2c717de369c66af62d34cdf52202da3951feab59 + React-microtasksnativemodule: c285dc3fa70ea3099d8ee0887aab0fc53f3ef791 + react-native-video: b18f2dda48b6424cb8d4d6168821223bbf9eb38a + react-native-video-plugin-sample: 79af31831cddfa916eb264d89b006fdcf227a8f5 React-nativeconfig: e90353e1c5ab46843222e25d529d342c174184f4 - React-NativeModulesApple: 5f46e9be9fbff00c706cb31a91753975e99f86a1 - React-perflogger: a629f2ce2eeab290cfc851a015c5d804a8b36f20 - React-performancetimeline: 09eaa1261ce7b3d51dcd8001f922c7b7f018957c + React-NativeModulesApple: de38a9d30fafd773bc72e8b078d57de2dfccae7f + React-perflogger: 63dd38ee6f413be0aa176de9f6efc65819e289ca + React-performancetimeline: 450a56eff8fbe3de667f26d1857f3bd150e037a7 React-RCTActionSheet: 0622ace751a55109844ff5d6239c83ae36f193d6 - React-RCTAnimation: 8651b17476b17d97d7f0fad16d9020f9b1724226 - React-RCTAppDelegate: 09147856b3e6c55f45afc32f485697b9c7097830 - React-RCTBlob: 4ce6e176c0b0999b6ae907537c665dec382095ca - React-RCTFabric: 6d6d776ec2bb746a599ca113da4c9e5e30519175 - React-RCTImage: 1ba821a26daf160f596266f78b084df38bad43bf - React-RCTLinking: b913efa56e459a7c9bd8575ea35fffcd43d2b226 - React-RCTNetwork: ff7b971a79b35407aa7fc84d0d34d4bf87977832 - React-RCTSettings: a40d2c591ea4b3ebeb0d5fca84cef83bf3a808b8 - React-RCTText: 54e4c01e54f2b323494950100b3f96a44c8d756a - React-RCTVibration: 415d77ace6572495a7fabc0013794eb232263f61 + React-RCTAnimation: 90e804d3918a97910c94732f23f1c3d3ac43d1ff + React-RCTAppDelegate: 7503e65922a7de2dcb04c7ad9dba8d18c0c1084b + React-RCTBlob: e0ac1d709fab205330a6a4ff3430b19358fe2aa4 + React-RCTFabric: a308ebf92877e49196d81568a0098bc5a54b0cc2 + React-RCTImage: 3ebf257e04f4e0d366e8d11e9ee0c9fb1d5318e4 + React-RCTLinking: 86a8f8fb5169e0499d8ff9f17b466103a9c7344f + React-RCTNetwork: 9d8dae79abf6281174aabdcd5cb952b125a19834 + React-RCTSettings: 26320c05739c8dc553e87f8195efb51fa1a4f4d1 + React-RCTText: 1b3aafa95d0b8f5546ea8b11f218b97a6c50e6ac + React-RCTVibration: b4b8c7ca82031b6a628d5197db32123786269f83 React-rendererconsistency: abd0c952e6447de1851995630d388adb756c5490 - React-rendererdebug: 931afa3e69cad5617d70fc8d22a51b8f0b8962ab + React-rendererdebug: d2cee4431aae3b94a369922950314bdd11ed4e36 React-rncore: 4fa27b1f5758f1e43daecaca33828b6724ff975e - React-RuntimeApple: bc87436ff646bc38b3b40b5c1b64b3292083b5d6 - React-RuntimeCore: 4588a5b4bf68324088d3aba640c7aecda1f8ccf7 + React-RuntimeApple: 4400921398bc7f37dea96b38408f75fa79a4e087 + React-RuntimeCore: f5d8891ca6c8fd0ea1ce97e1e742f674c6e3f3de React-runtimeexecutor: 369f2c0dd457ecc25d8aca20717b8b7c655fea30 - React-RuntimeHermes: fa0dbe3e215052f6f2a64961a81292a7d8a29cc1 - React-runtimescheduler: 0c3791a76e2c851a6c0387549f6288cb9dd6e3d6 + React-RuntimeHermes: f36d89a2b787488c5c52b56f6e390ea6daad8177 + React-runtimescheduler: d767ba9dbcc56926ef8e33d9a79ffc73b1f21216 React-timing: d1b4d886b3ad54cb7d7902551d6f326de16b13d3 - React-utils: 5b35e0df4b1e5414ccb7aad3295de9e422d19265 - ReactCodegen: 64ba3fd7eb98692015dbd29ca0ca38ea2c5f92c6 - ReactCommon: 8676c2ee5eb8c937ea33df7c91f42d4cd9e95e79 - RNCPicker: f963e01f78e546a93b98aa201501713dbda14e94 + React-utils: b5263797499f58d23f8cdf84a43947fa935144a5 + ReactCodegen: be823df68a7af474545f249dab02cd94893917e8 + ReactCommon: ccfca2ddb61b208d3a9bfbbfc87233656d65a4d8 + RNCPicker: 0e683a85cae99a8a739e97a49f269a4630de0a01 SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 - Yoga: aaa386a35c44b356306dafca9e8afd81877fb7ed + Yoga: 748384d41d07db337d313e0f37eec81c8c0ae46f PODFILE CHECKSUM: 90d803972b4acfc1d2bb6dbe7b0713677a2ff655 -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2 diff --git a/ios/Video/DataStructures/AdParams.swift b/ios/Video/DataStructures/AdParams.swift index be97fe0a..26c0a25e 100644 --- a/ios/Video/DataStructures/AdParams.swift +++ b/ios/Video/DataStructures/AdParams.swift @@ -1,4 +1,4 @@ -struct AdParams { +public struct AdParams { let adTagUrl: String? let adLanguage: String? diff --git a/ios/Video/DataStructures/CustomMetadata.swift b/ios/Video/DataStructures/CustomMetadata.swift index 9526d680..8a38abf6 100644 --- a/ios/Video/DataStructures/CustomMetadata.swift +++ b/ios/Video/DataStructures/CustomMetadata.swift @@ -1,4 +1,4 @@ -struct CustomMetadata { +public struct CustomMetadata { let title: String? let subtitle: String? let artist: String? diff --git a/ios/Video/DataStructures/OverridePlayerAsset.swift b/ios/Video/DataStructures/OverridePlayerAsset.swift new file mode 100644 index 00000000..07504e0d --- /dev/null +++ b/ios/Video/DataStructures/OverridePlayerAsset.swift @@ -0,0 +1,24 @@ +import AVFoundation + +// MARK: - OverridePlayerAssetType + +public enum OverridePlayerAssetType { + // Return partially modified asset, that will go through the default prepare process + case partial + // Return fully modified asset, that will skip the default prepare process + case full +} + +public typealias OverridePlayerAsset = (VideoSource, AVAsset) async -> OverridePlayerAssetResult + +// MARK: - OverridePlayerAssetResult + +public struct OverridePlayerAssetResult { + public let type: OverridePlayerAssetType + public let asset: AVAsset + + public init(type: OverridePlayerAssetType, asset: AVAsset) { + self.type = type + self.asset = asset + } +} diff --git a/ios/Video/DataStructures/TextTrack.swift b/ios/Video/DataStructures/TextTrack.swift index 4c186b28..f25bbd31 100644 --- a/ios/Video/DataStructures/TextTrack.swift +++ b/ios/Video/DataStructures/TextTrack.swift @@ -1,4 +1,4 @@ -struct TextTrack { +public struct TextTrack { let type: String let language: String let title: String diff --git a/ios/Video/DataStructures/VideoSource.swift b/ios/Video/DataStructures/VideoSource.swift index 1f367149..0a5e890c 100644 --- a/ios/Video/DataStructures/VideoSource.swift +++ b/ios/Video/DataStructures/VideoSource.swift @@ -1,4 +1,4 @@ -struct VideoSource { +public struct VideoSource { let type: String? let uri: String? let isNetwork: Bool diff --git a/ios/Video/RCTVideo.swift b/ios/Video/RCTVideo.swift index 369d465f..94f1e8f0 100644 --- a/ios/Video/RCTVideo.swift +++ b/ios/Video/RCTVideo.swift @@ -493,7 +493,19 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH ]) if let uri = source.uri, uri.starts(with: "ph://") { - let photoAsset = await RCTVideoUtils.preparePHAsset(uri: uri) + guard let photoAsset = await RCTVideoUtils.preparePHAsset(uri: uri) else { + DebugLog("Could not load asset '\(String(describing: _source))'") + throw NSError(domain: "", code: 0, userInfo: nil) + } + + if let overridePlayerAsset = await ReactNativeVideoManager.shared.overridePlayerAsset(source: source, asset: photoAsset) { + if overridePlayerAsset.type == .full { + return AVPlayerItem(asset: overridePlayerAsset.asset) + } + + return await playerItemPrepareText(source: source, asset: overridePlayerAsset.asset, assetOptions: nil, uri: source.uri ?? "") + } + return await playerItemPrepareText(source: source, asset: photoAsset, assetOptions: nil, uri: source.uri ?? "") } @@ -530,6 +542,14 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH ) } + if let overridePlayerAsset = await ReactNativeVideoManager.shared.overridePlayerAsset(source: source, asset: asset) { + if overridePlayerAsset.type == .full { + return AVPlayerItem(asset: overridePlayerAsset.asset) + } + + return await playerItemPrepareText(source: source, asset: overridePlayerAsset.asset, assetOptions: assetOptions, uri: source.uri ?? "") + } + return await playerItemPrepareText(source: source, asset: asset, assetOptions: assetOptions, uri: source.uri ?? "") } diff --git a/ios/Video/RNVAVPlayerPlugin.swift b/ios/Video/RNVAVPlayerPlugin.swift index 1e62d55d..c6c528e8 100644 --- a/ios/Video/RNVAVPlayerPlugin.swift +++ b/ios/Video/RNVAVPlayerPlugin.swift @@ -36,6 +36,14 @@ open class RNVAVPlayerPlugin: RNVPlugin { */ open func onInstanceRemoved(id _: String, player _: AVPlayer) { /* no-op */ } + /** + * Function called when creating a new AVPlayerItem + * @param source: The VideoSource describing the video (uri, type, headers, etc.) + * @param asset: The AVAsset prepared by the player + * @return: OverridePlayerAssetResult if you want to override, or nil if you don't + */ + open func overridePlayerAsset(source _: VideoSource, asset _: AVAsset) async -> OverridePlayerAssetResult? { nil } + // MARK: - RNVPlugin methods override public func onInstanceCreated(id: String, player: Any) { diff --git a/ios/Video/ReactNativeVideoManager.swift b/ios/Video/ReactNativeVideoManager.swift index d5efbcad..61c6a7ce 100644 --- a/ios/Video/ReactNativeVideoManager.swift +++ b/ios/Video/ReactNativeVideoManager.swift @@ -3,6 +3,7 @@ // react-native-video // +import AVFoundation import Foundation public class ReactNativeVideoManager: RNVPlugin { @@ -67,6 +68,17 @@ public class ReactNativeVideoManager: RNVPlugin { return customDRMManager?.1 ?? DRMManager() } + public func overridePlayerAsset(source: VideoSource, asset: AVAsset) async -> OverridePlayerAssetResult? { + for plugin in pluginList { + if let avpPlugin = plugin as? RNVAVPlayerPlugin, + let overridePlayerAsset = await avpPlugin.overridePlayerAsset(source: source, asset: asset) { + return overridePlayerAsset + } + } + + return nil + } + // MARK: - Helper methods func maybeRegisterAVPlayerPlugin(plugin: RNVPlugin) {