react-native-media-view 0.2.0
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/LICENSE +20 -0
- package/README.md +112 -0
- package/android/build.gradle +67 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/mediaview/MediaView.kt +15 -0
- package/android/src/main/java/com/mediaview/MediaViewManager.kt +41 -0
- package/android/src/main/java/com/mediaview/MediaViewPackage.kt +17 -0
- package/ios/MediaImageView.swift +293 -0
- package/ios/MediaView.h +15 -0
- package/ios/MediaView.mm +105 -0
- package/lib/module/MediaViewNativeComponent.ts +41 -0
- package/lib/module/index.js +37 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/react-native.d.js +2 -0
- package/lib/module/react-native.d.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/MediaViewNativeComponent.d.ts +37 -0
- package/lib/typescript/src/MediaViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +23 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/package.json +165 -0
- package/react-native-media-view.podspec +20 -0
- package/src/MediaViewNativeComponent.ts +41 -0
- package/src/index.tsx +49 -0
- package/src/react-native.d.ts +31 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Darkce
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
in the Software without restriction, including without limitation the rights
|
|
7
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
furnished to do so, subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# react-native-media-view
|
|
2
|
+
|
|
3
|
+
A high-performance React Native component for displaying images (including AVIF) and videos using native platform APIs. Renders images via `UIImageView` and videos via `AVKit` — no WebView overhead. Supports iOS only with the new architecture (Fabric).
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Native image rendering** — Uses `UIImageView` with `UIImage` for fast, memory-efficient display
|
|
8
|
+
- **AVIF support** — iOS 16+ decodes AVIF natively, including animated AVIF
|
|
9
|
+
- **Native video playback** — Uses `AVQueuePlayer` + `AVPlayerLayer` with seamless looping
|
|
10
|
+
- **Resize modes** — `contain`, `cover`, `stretch`, `center` (maps to `contentMode` / `videoGravity`)
|
|
11
|
+
- **Lifecycle events** — `onLoadStart`, `onLoad`, `onLoadEnd`, `onError`
|
|
12
|
+
- **No WebView, no third-party dependencies**
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
npm install react-native-media-view
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
For iOS, install pods:
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
cd ios && pod install
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Metro Configuration
|
|
27
|
+
|
|
28
|
+
Add `avif` to your Metro asset extensions in `metro.config.js`:
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
const { getDefaultConfig } = require('@react-native/metro-config');
|
|
32
|
+
|
|
33
|
+
const config = getDefaultConfig(__dirname);
|
|
34
|
+
config.resolver.assetExts.push('avif');
|
|
35
|
+
|
|
36
|
+
module.exports = config;
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
import { MediaView } from 'react-native-media-view';
|
|
43
|
+
|
|
44
|
+
// Local AVIF image
|
|
45
|
+
<MediaView
|
|
46
|
+
source={require('./assets/image.avif')}
|
|
47
|
+
resizeMode="contain"
|
|
48
|
+
style={{ width: 300, height: 300 }}
|
|
49
|
+
onLoad={() => console.log('Loaded')}
|
|
50
|
+
onError={(e) => console.error(e.nativeEvent.error)}
|
|
51
|
+
/>
|
|
52
|
+
|
|
53
|
+
// Remote image
|
|
54
|
+
<MediaView
|
|
55
|
+
source={{ uri: 'https://example.com/photo.avif' }}
|
|
56
|
+
resizeMode="cover"
|
|
57
|
+
style={{ width: '100%', aspectRatio: 16 / 9 }}
|
|
58
|
+
/>
|
|
59
|
+
|
|
60
|
+
// Video (auto-detected by file extension, loops + muted)
|
|
61
|
+
<MediaView
|
|
62
|
+
source={{ uri: 'https://example.com/clip.mp4' }}
|
|
63
|
+
resizeMode="cover"
|
|
64
|
+
style={{ width: '100%', height: 200 }}
|
|
65
|
+
/>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Props
|
|
69
|
+
|
|
70
|
+
| Prop | Type | Default | Description |
|
|
71
|
+
| ------------ | ----------------------------------------------- | ------------ | ------------------------------------------ |
|
|
72
|
+
| `source` | `ImageRequireSource \| { uri: string }` | **required** | Image or video source (`require()` or URI) |
|
|
73
|
+
| `resizeMode` | `'contain' \| 'cover' \| 'stretch' \| 'center'` | `'contain'` | How media fits the view |
|
|
74
|
+
| `style` | `ViewStyle` | — | Standard React Native view style |
|
|
75
|
+
|
|
76
|
+
## Events
|
|
77
|
+
|
|
78
|
+
| Event | Payload | Description |
|
|
79
|
+
| ------------- | ----------- | ------------------------------------- |
|
|
80
|
+
| `onLoadStart` | — | Loading has begun |
|
|
81
|
+
| `onLoad` | — | Media is ready for display / playback |
|
|
82
|
+
| `onLoadEnd` | — | Loading finished (success or failure) |
|
|
83
|
+
| `onError` | `{ error }` | An error occurred |
|
|
84
|
+
|
|
85
|
+
## How It Works
|
|
86
|
+
|
|
87
|
+
| Media type | Rendered with |
|
|
88
|
+
| ---------- | --------------------------------- |
|
|
89
|
+
| Image | `UIImageView` (`UIImage(data:)`) |
|
|
90
|
+
| Video | `AVQueuePlayer` + `AVPlayerLayer` |
|
|
91
|
+
|
|
92
|
+
- **Images** are loaded asynchronously (file or network) and decoded via `UIImage`, which supports AVIF on iOS 16+.
|
|
93
|
+
- **Videos** are detected by file extension (`.mp4`, `.mov`, `.webm`, etc.) and played with `AVQueuePlayer` + `AVPlayerLooper` for seamless looping. Playback is muted by default.
|
|
94
|
+
|
|
95
|
+
## Requirements
|
|
96
|
+
|
|
97
|
+
- iOS 16.0+
|
|
98
|
+
- React Native 0.74+ (New Architecture / Fabric)
|
|
99
|
+
|
|
100
|
+
## Contributing
|
|
101
|
+
|
|
102
|
+
- [Development workflow](CONTRIBUTING.md#development-workflow)
|
|
103
|
+
- [Sending a pull request](CONTRIBUTING.md#sending-a-pull-request)
|
|
104
|
+
- [Code of conduct](CODE_OF_CONDUCT.md)
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
MIT
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
buildscript {
|
|
2
|
+
ext.MediaView = [
|
|
3
|
+
kotlinVersion: "2.0.21",
|
|
4
|
+
minSdkVersion: 24,
|
|
5
|
+
compileSdkVersion: 36,
|
|
6
|
+
targetSdkVersion: 36
|
|
7
|
+
]
|
|
8
|
+
|
|
9
|
+
ext.getExtOrDefault = { prop ->
|
|
10
|
+
if (rootProject.ext.has(prop)) {
|
|
11
|
+
return rootProject.ext.get(prop)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return MediaView[prop]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
repositories {
|
|
18
|
+
google()
|
|
19
|
+
mavenCentral()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
dependencies {
|
|
23
|
+
classpath "com.android.tools.build:gradle:8.7.2"
|
|
24
|
+
// noinspection DifferentKotlinGradleVersion
|
|
25
|
+
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
apply plugin: "com.android.library"
|
|
31
|
+
apply plugin: "kotlin-android"
|
|
32
|
+
|
|
33
|
+
apply plugin: "com.facebook.react"
|
|
34
|
+
|
|
35
|
+
android {
|
|
36
|
+
namespace "com.mediaview"
|
|
37
|
+
|
|
38
|
+
compileSdkVersion getExtOrDefault("compileSdkVersion")
|
|
39
|
+
|
|
40
|
+
defaultConfig {
|
|
41
|
+
minSdkVersion getExtOrDefault("minSdkVersion")
|
|
42
|
+
targetSdkVersion getExtOrDefault("targetSdkVersion")
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
buildFeatures {
|
|
46
|
+
buildConfig true
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
buildTypes {
|
|
50
|
+
release {
|
|
51
|
+
minifyEnabled false
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
lint {
|
|
56
|
+
disable "GradleCompatible"
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
compileOptions {
|
|
60
|
+
sourceCompatibility JavaVersion.VERSION_1_8
|
|
61
|
+
targetCompatibility JavaVersion.VERSION_1_8
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
dependencies {
|
|
66
|
+
implementation "com.facebook.react:react-android"
|
|
67
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
package com.mediaview
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.util.AttributeSet
|
|
5
|
+
import android.view.View
|
|
6
|
+
|
|
7
|
+
class MediaView : View {
|
|
8
|
+
constructor(context: Context?) : super(context)
|
|
9
|
+
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
|
|
10
|
+
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(
|
|
11
|
+
context,
|
|
12
|
+
attrs,
|
|
13
|
+
defStyleAttr
|
|
14
|
+
)
|
|
15
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
package com.mediaview
|
|
2
|
+
|
|
3
|
+
import android.graphics.Color
|
|
4
|
+
import com.facebook.react.module.annotations.ReactModule
|
|
5
|
+
import com.facebook.react.uimanager.SimpleViewManager
|
|
6
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
7
|
+
import com.facebook.react.uimanager.ViewManagerDelegate
|
|
8
|
+
import com.facebook.react.uimanager.annotations.ReactProp
|
|
9
|
+
import com.facebook.react.viewmanagers.MediaViewManagerInterface
|
|
10
|
+
import com.facebook.react.viewmanagers.MediaViewManagerDelegate
|
|
11
|
+
|
|
12
|
+
@ReactModule(name = MediaViewManager.NAME)
|
|
13
|
+
class MediaViewManager : SimpleViewManager<MediaView>(),
|
|
14
|
+
MediaViewManagerInterface<MediaView> {
|
|
15
|
+
private val mDelegate: ViewManagerDelegate<MediaView>
|
|
16
|
+
|
|
17
|
+
init {
|
|
18
|
+
mDelegate = MediaViewManagerDelegate(this)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
override fun getDelegate(): ViewManagerDelegate<MediaView>? {
|
|
22
|
+
return mDelegate
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
override fun getName(): String {
|
|
26
|
+
return NAME
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public override fun createViewInstance(context: ThemedReactContext): MediaView {
|
|
30
|
+
return MediaView(context)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@ReactProp(name = "color")
|
|
34
|
+
override fun setColor(view: MediaView?, color: Int?) {
|
|
35
|
+
view?.setBackgroundColor(color ?: Color.TRANSPARENT)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
companion object {
|
|
39
|
+
const val NAME = "MediaView"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
package com.mediaview
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.BaseReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.module.model.ReactModuleInfoProvider
|
|
7
|
+
import com.facebook.react.uimanager.ViewManager
|
|
8
|
+
|
|
9
|
+
class MediaViewPackage : BaseReactPackage() {
|
|
10
|
+
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
|
|
11
|
+
return listOf(MediaViewManager())
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? = null
|
|
15
|
+
|
|
16
|
+
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider = ReactModuleInfoProvider { emptyMap() }
|
|
17
|
+
}
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
//
|
|
2
|
+
// MediaImageView.swift
|
|
3
|
+
// react-native-media-view
|
|
4
|
+
//
|
|
5
|
+
// Native media view using UIImageView (for images) and AVKit (for video)
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
import AVFoundation
|
|
9
|
+
import AVKit
|
|
10
|
+
import Foundation
|
|
11
|
+
import UIKit
|
|
12
|
+
|
|
13
|
+
/// Delegate protocol for handling media view events
|
|
14
|
+
@objc public protocol MediaImageViewDelegate: AnyObject {
|
|
15
|
+
func handleOnLoadStart()
|
|
16
|
+
func handleOnLoad()
|
|
17
|
+
func handleOnLoadEnd()
|
|
18
|
+
func handleOnError(error: String)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/// Native UIView subclass for displaying images and video content
|
|
22
|
+
@objcMembers
|
|
23
|
+
public class MediaImageViewCore: UIView {
|
|
24
|
+
public weak var delegate: MediaImageViewDelegate?
|
|
25
|
+
private var currentURI: String?
|
|
26
|
+
private var currentResizeMode: String = "contain"
|
|
27
|
+
|
|
28
|
+
// MARK: - Image display
|
|
29
|
+
|
|
30
|
+
private lazy var imageView: UIImageView = {
|
|
31
|
+
let iv = UIImageView()
|
|
32
|
+
iv.clipsToBounds = true
|
|
33
|
+
iv.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
34
|
+
iv.backgroundColor = .clear
|
|
35
|
+
return iv
|
|
36
|
+
}()
|
|
37
|
+
|
|
38
|
+
// MARK: - Video display
|
|
39
|
+
|
|
40
|
+
private var queuePlayer: AVQueuePlayer?
|
|
41
|
+
private var playerLayer: AVPlayerLayer?
|
|
42
|
+
private var playerLooper: AVPlayerLooper?
|
|
43
|
+
private var playerItem: AVPlayerItem?
|
|
44
|
+
private var statusObservation: NSKeyValueObservation?
|
|
45
|
+
private var loadingTask: URLSessionDataTask?
|
|
46
|
+
|
|
47
|
+
// MARK: - Init
|
|
48
|
+
|
|
49
|
+
public override init(frame: CGRect) {
|
|
50
|
+
super.init(frame: frame)
|
|
51
|
+
setupView()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
public required init?(coder: NSCoder) {
|
|
55
|
+
super.init(coder: coder)
|
|
56
|
+
setupView()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private func setupView() {
|
|
60
|
+
clipsToBounds = true
|
|
61
|
+
backgroundColor = .clear
|
|
62
|
+
isUserInteractionEnabled = false
|
|
63
|
+
imageView.frame = bounds
|
|
64
|
+
addSubview(imageView)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// MARK: - Layout
|
|
68
|
+
|
|
69
|
+
public override func layoutSubviews() {
|
|
70
|
+
super.layoutSubviews()
|
|
71
|
+
imageView.frame = bounds
|
|
72
|
+
playerLayer?.frame = bounds
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// MARK: - Video detection
|
|
76
|
+
|
|
77
|
+
private static let videoExtensions: Set<String> = [
|
|
78
|
+
"mp4", "webm", "mov", "m4v", "avi", "mkv", "ogg", "ogv",
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
private func isVideoURL(_ urlString: String) -> Bool {
|
|
82
|
+
guard let url = URL(string: urlString) else { return false }
|
|
83
|
+
let ext = url.pathExtension.lowercased()
|
|
84
|
+
return Self.videoExtensions.contains(ext)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// MARK: - Public API
|
|
88
|
+
|
|
89
|
+
public func setSource(_ source: NSDictionary) {
|
|
90
|
+
guard let uri = source["uri"] as? String, !uri.isEmpty else {
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Avoid reloading the same URI
|
|
95
|
+
if uri == currentURI { return }
|
|
96
|
+
|
|
97
|
+
delegate?.handleOnLoadStart()
|
|
98
|
+
currentURI = uri
|
|
99
|
+
|
|
100
|
+
// Clean up previous content
|
|
101
|
+
cleanupVideo()
|
|
102
|
+
cancelImageLoading()
|
|
103
|
+
imageView.image = nil
|
|
104
|
+
|
|
105
|
+
if isVideoURL(uri) {
|
|
106
|
+
loadVideo(uri: uri)
|
|
107
|
+
} else {
|
|
108
|
+
loadImage(uri: uri)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
public func setResizeMode(_ resizeMode: String?) {
|
|
113
|
+
guard let resizeMode, resizeMode != currentResizeMode else { return }
|
|
114
|
+
currentResizeMode = resizeMode
|
|
115
|
+
applyResizeMode()
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// MARK: - Image Loading
|
|
119
|
+
|
|
120
|
+
private func loadImage(uri: String) {
|
|
121
|
+
imageView.isHidden = false
|
|
122
|
+
|
|
123
|
+
guard let url = URL(string: uri) else {
|
|
124
|
+
delegate?.handleOnError(error: "Invalid image URI")
|
|
125
|
+
delegate?.handleOnLoadEnd()
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if url.isFileURL {
|
|
130
|
+
loadImageFromFile(url: url)
|
|
131
|
+
} else {
|
|
132
|
+
loadImageFromNetwork(url: url)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private func loadImageFromFile(url: URL) {
|
|
137
|
+
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
|
|
138
|
+
guard let self else { return }
|
|
139
|
+
do {
|
|
140
|
+
let data = try Data(contentsOf: url)
|
|
141
|
+
guard let image = UIImage(data: data) else {
|
|
142
|
+
DispatchQueue.main.async {
|
|
143
|
+
self.delegate?.handleOnError(error: "Failed to decode image")
|
|
144
|
+
self.delegate?.handleOnLoadEnd()
|
|
145
|
+
}
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
DispatchQueue.main.async {
|
|
149
|
+
guard self.currentURI == url.absoluteString else { return }
|
|
150
|
+
self.imageView.image = image
|
|
151
|
+
self.applyResizeMode()
|
|
152
|
+
self.delegate?.handleOnLoad()
|
|
153
|
+
self.delegate?.handleOnLoadEnd()
|
|
154
|
+
}
|
|
155
|
+
} catch {
|
|
156
|
+
DispatchQueue.main.async {
|
|
157
|
+
self.delegate?.handleOnError(error: error.localizedDescription)
|
|
158
|
+
self.delegate?.handleOnLoadEnd()
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private func loadImageFromNetwork(url: URL) {
|
|
165
|
+
let task = URLSession.shared.dataTask(with: url) { [weak self] data, _, error in
|
|
166
|
+
guard let self else { return }
|
|
167
|
+
DispatchQueue.main.async {
|
|
168
|
+
guard self.currentURI == url.absoluteString else { return }
|
|
169
|
+
|
|
170
|
+
if let error {
|
|
171
|
+
self.delegate?.handleOnError(error: error.localizedDescription)
|
|
172
|
+
self.delegate?.handleOnLoadEnd()
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
guard let data, let image = UIImage(data: data) else {
|
|
177
|
+
self.delegate?.handleOnError(error: "Failed to decode image from network")
|
|
178
|
+
self.delegate?.handleOnLoadEnd()
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
self.imageView.image = image
|
|
183
|
+
self.applyResizeMode()
|
|
184
|
+
self.delegate?.handleOnLoad()
|
|
185
|
+
self.delegate?.handleOnLoadEnd()
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
loadingTask = task
|
|
189
|
+
task.resume()
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private func cancelImageLoading() {
|
|
193
|
+
loadingTask?.cancel()
|
|
194
|
+
loadingTask = nil
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// MARK: - Video Loading
|
|
198
|
+
|
|
199
|
+
private func loadVideo(uri: String) {
|
|
200
|
+
imageView.isHidden = true
|
|
201
|
+
|
|
202
|
+
guard let url = URL(string: uri) else {
|
|
203
|
+
delegate?.handleOnError(error: "Invalid video URI")
|
|
204
|
+
delegate?.handleOnLoadEnd()
|
|
205
|
+
return
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
let item = AVPlayerItem(url: url)
|
|
209
|
+
playerItem = item
|
|
210
|
+
|
|
211
|
+
let player = AVQueuePlayer()
|
|
212
|
+
queuePlayer = player
|
|
213
|
+
player.isMuted = true
|
|
214
|
+
|
|
215
|
+
// Loop playback indefinitely — looper manages item insertion
|
|
216
|
+
playerLooper = AVPlayerLooper(player: player, templateItem: item)
|
|
217
|
+
|
|
218
|
+
let pLayer = AVPlayerLayer(player: player)
|
|
219
|
+
pLayer.frame = bounds
|
|
220
|
+
playerLayer = pLayer
|
|
221
|
+
layer.addSublayer(pLayer)
|
|
222
|
+
applyVideoGravity()
|
|
223
|
+
|
|
224
|
+
// Observe the player's currentItem status for readiness
|
|
225
|
+
statusObservation = player.observe(\.currentItem?.status, options: [.new]) {
|
|
226
|
+
[weak self] observedPlayer, _ in
|
|
227
|
+
DispatchQueue.main.async {
|
|
228
|
+
guard let self else { return }
|
|
229
|
+
guard let currentItem = observedPlayer.currentItem else { return }
|
|
230
|
+
switch currentItem.status {
|
|
231
|
+
case .readyToPlay:
|
|
232
|
+
self.delegate?.handleOnLoad()
|
|
233
|
+
self.delegate?.handleOnLoadEnd()
|
|
234
|
+
observedPlayer.play()
|
|
235
|
+
case .failed:
|
|
236
|
+
let msg = currentItem.error?.localizedDescription ?? "Unknown video error"
|
|
237
|
+
self.delegate?.handleOnError(error: msg)
|
|
238
|
+
self.delegate?.handleOnLoadEnd()
|
|
239
|
+
default:
|
|
240
|
+
break
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private func cleanupVideo() {
|
|
247
|
+
statusObservation?.invalidate()
|
|
248
|
+
statusObservation = nil
|
|
249
|
+
queuePlayer?.pause()
|
|
250
|
+
queuePlayer = nil
|
|
251
|
+
playerLooper = nil
|
|
252
|
+
playerItem = nil
|
|
253
|
+
playerLayer?.removeFromSuperlayer()
|
|
254
|
+
playerLayer = nil
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// MARK: - Resize Mode
|
|
258
|
+
|
|
259
|
+
private func applyResizeMode() {
|
|
260
|
+
switch currentResizeMode {
|
|
261
|
+
case "cover":
|
|
262
|
+
imageView.contentMode = .scaleAspectFill
|
|
263
|
+
case "contain":
|
|
264
|
+
imageView.contentMode = .scaleAspectFit
|
|
265
|
+
case "stretch":
|
|
266
|
+
imageView.contentMode = .scaleToFill
|
|
267
|
+
case "center":
|
|
268
|
+
imageView.contentMode = .center
|
|
269
|
+
default:
|
|
270
|
+
imageView.contentMode = .scaleAspectFit
|
|
271
|
+
}
|
|
272
|
+
applyVideoGravity()
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
private func applyVideoGravity() {
|
|
276
|
+
guard let playerLayer else { return }
|
|
277
|
+
switch currentResizeMode {
|
|
278
|
+
case "cover":
|
|
279
|
+
playerLayer.videoGravity = .resizeAspectFill
|
|
280
|
+
case "stretch":
|
|
281
|
+
playerLayer.videoGravity = .resize
|
|
282
|
+
default:
|
|
283
|
+
playerLayer.videoGravity = .resizeAspect
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// MARK: - Cleanup
|
|
288
|
+
|
|
289
|
+
deinit {
|
|
290
|
+
cleanupVideo()
|
|
291
|
+
cancelImageLoading()
|
|
292
|
+
}
|
|
293
|
+
}
|
package/ios/MediaView.h
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#import <React/RCTViewComponentView.h>
|
|
2
|
+
#import <UIKit/UIKit.h>
|
|
3
|
+
|
|
4
|
+
#ifndef MediaViewNativeComponent_h
|
|
5
|
+
#define MediaViewNativeComponent_h
|
|
6
|
+
|
|
7
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
8
|
+
|
|
9
|
+
@interface MediaView : RCTViewComponentView
|
|
10
|
+
|
|
11
|
+
@end
|
|
12
|
+
|
|
13
|
+
NS_ASSUME_NONNULL_END
|
|
14
|
+
|
|
15
|
+
#endif /* MediaViewNativeComponent_h */
|
package/ios/MediaView.mm
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
//
|
|
2
|
+
// MediaView.mm
|
|
3
|
+
// react-native-media-view
|
|
4
|
+
//
|
|
5
|
+
// React Native Fabric component binding for media view
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
#import "MediaView.h"
|
|
9
|
+
#if __has_include(<react_native_media_view/react_native_media_view-Swift.h>)
|
|
10
|
+
#import <react_native_media_view/react_native_media_view-Swift.h>
|
|
11
|
+
#else
|
|
12
|
+
#import "react_native_media_view-Swift.h"
|
|
13
|
+
#endif
|
|
14
|
+
#import <React/RCTConversions.h>
|
|
15
|
+
|
|
16
|
+
#import <react/renderer/components/MediaViewSpec/ComponentDescriptors.h>
|
|
17
|
+
#import <react/renderer/components/MediaViewSpec/EventEmitters.h>
|
|
18
|
+
#import <react/renderer/components/MediaViewSpec/Props.h>
|
|
19
|
+
#import <react/renderer/components/MediaViewSpec/RCTComponentViewHelpers.h>
|
|
20
|
+
|
|
21
|
+
#import "RCTFabricComponentsPlugins.h"
|
|
22
|
+
|
|
23
|
+
using namespace facebook::react;
|
|
24
|
+
|
|
25
|
+
@interface MediaView () <MediaImageViewDelegate, RCTMediaViewViewProtocol>
|
|
26
|
+
@end
|
|
27
|
+
|
|
28
|
+
@implementation MediaView {
|
|
29
|
+
MediaImageViewCore *_view;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
+ (ComponentDescriptorProvider)componentDescriptorProvider {
|
|
33
|
+
return concreteComponentDescriptorProvider<MediaViewComponentDescriptor>();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
- (instancetype)initWithFrame:(CGRect)frame {
|
|
37
|
+
if (self = [super initWithFrame:frame]) {
|
|
38
|
+
static const auto defaultProps = std::make_shared<const MediaViewProps>();
|
|
39
|
+
_props = defaultProps;
|
|
40
|
+
|
|
41
|
+
_view = [[MediaImageViewCore alloc] init];
|
|
42
|
+
_view.delegate = self;
|
|
43
|
+
|
|
44
|
+
self.contentView = _view;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return self;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
- (void)updateProps:(Props::Shared const &)props
|
|
51
|
+
oldProps:(Props::Shared const &)oldProps {
|
|
52
|
+
const auto &oldViewProps =
|
|
53
|
+
*std::static_pointer_cast<MediaViewProps const>(_props);
|
|
54
|
+
const auto &newViewProps =
|
|
55
|
+
*std::static_pointer_cast<MediaViewProps const>(props);
|
|
56
|
+
|
|
57
|
+
// Update source
|
|
58
|
+
if (oldViewProps.source.uri != newViewProps.source.uri) {
|
|
59
|
+
NSDictionary *sourceDict =
|
|
60
|
+
@{@"uri" : RCTNSStringFromString(newViewProps.source.uri)};
|
|
61
|
+
[_view setSource:sourceDict];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Update resizeMode (aligned with React Native Image)
|
|
65
|
+
if (oldViewProps.resizeMode != newViewProps.resizeMode) {
|
|
66
|
+
[_view setResizeMode:RCTNSStringFromString(newViewProps.resizeMode)];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
[super updateProps:props oldProps:oldProps];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
#pragma mark - MediaImageViewDelegate
|
|
73
|
+
|
|
74
|
+
- (void)handleOnLoadStart {
|
|
75
|
+
if (_eventEmitter != nil) {
|
|
76
|
+
std::dynamic_pointer_cast<const MediaViewEventEmitter>(_eventEmitter)
|
|
77
|
+
->onLoadStart(MediaViewEventEmitter::OnLoadStart{});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
- (void)handleOnLoad {
|
|
82
|
+
if (_eventEmitter != nil) {
|
|
83
|
+
std::dynamic_pointer_cast<const MediaViewEventEmitter>(_eventEmitter)
|
|
84
|
+
->onLoad(MediaViewEventEmitter::OnLoad{});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
- (void)handleOnLoadEnd {
|
|
89
|
+
if (_eventEmitter != nil) {
|
|
90
|
+
std::dynamic_pointer_cast<const MediaViewEventEmitter>(_eventEmitter)
|
|
91
|
+
->onLoadEnd(MediaViewEventEmitter::OnLoadEnd{});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
- (void)handleOnErrorWithError:(NSString *)error {
|
|
96
|
+
if (_eventEmitter != nil) {
|
|
97
|
+
std::dynamic_pointer_cast<const MediaViewEventEmitter>(_eventEmitter)
|
|
98
|
+
->onError(MediaViewEventEmitter::OnError{
|
|
99
|
+
.error = std::string([error UTF8String])});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
@end
|
|
104
|
+
|
|
105
|
+
Class<RCTComponentViewProtocol> MediaViewCls(void) { return MediaView.class; }
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { codegenNativeComponent } from 'react-native';
|
|
2
|
+
import type { ViewProps } from 'react-native';
|
|
3
|
+
import type { DirectEventHandler } from 'react-native/Libraries/Types/CodegenTypes';
|
|
4
|
+
import type { HostComponent } from 'react-native';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Source props for media (resolved from require())
|
|
8
|
+
*/
|
|
9
|
+
export interface MediaSourceProps {
|
|
10
|
+
/** URI of the image or video */
|
|
11
|
+
uri?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Resize mode for image display (aligned with React Native Image)
|
|
16
|
+
*/
|
|
17
|
+
export type ResizeMode = 'cover' | 'contain' | 'stretch' | 'center';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Native props for MediaView component
|
|
21
|
+
*/
|
|
22
|
+
interface NativeProps extends ViewProps {
|
|
23
|
+
/** Source of the media */
|
|
24
|
+
source?: MediaSourceProps;
|
|
25
|
+
/** Resize mode for image display (aligned with React Native Image) */
|
|
26
|
+
resizeMode?: string;
|
|
27
|
+
/** Callback when loading starts */
|
|
28
|
+
onLoadStart?: DirectEventHandler<null>;
|
|
29
|
+
/** Callback when the image is loaded */
|
|
30
|
+
onLoad?: DirectEventHandler<null>;
|
|
31
|
+
/** Callback when loading ends (success or failure) */
|
|
32
|
+
onLoadEnd?: DirectEventHandler<null>;
|
|
33
|
+
/** Callback when an error occurs */
|
|
34
|
+
onError?: DirectEventHandler<Readonly<{ error: string }>>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type MediaViewComponent = HostComponent<NativeProps>;
|
|
38
|
+
|
|
39
|
+
export default codegenNativeComponent<NativeProps>(
|
|
40
|
+
'MediaView'
|
|
41
|
+
) as MediaViewComponent;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { Image } from 'react-native';
|
|
4
|
+
import MediaViewNativeComponent from './MediaViewNativeComponent';
|
|
5
|
+
|
|
6
|
+
/** @deprecated Use ResizeMode instead */
|
|
7
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
8
|
+
/**
|
|
9
|
+
* MediaView - A React Native component for displaying images and videos
|
|
10
|
+
* Supports AVIF, PNG, JPEG, GIF, WebP images, and video files (mp4, webm, mov, etc.)
|
|
11
|
+
* Video URIs are automatically detected by file extension and rendered with autoplay, loop, muted, playsinline.
|
|
12
|
+
*/
|
|
13
|
+
export function MediaView(props) {
|
|
14
|
+
const {
|
|
15
|
+
source,
|
|
16
|
+
resizeMode = 'contain',
|
|
17
|
+
...restProps
|
|
18
|
+
} = props;
|
|
19
|
+
let resolvedSource;
|
|
20
|
+
if (typeof source === 'object' && 'uri' in source) {
|
|
21
|
+
resolvedSource = {
|
|
22
|
+
uri: source.uri
|
|
23
|
+
};
|
|
24
|
+
} else {
|
|
25
|
+
const resolved = Image.resolveAssetSource(source);
|
|
26
|
+
resolvedSource = {
|
|
27
|
+
uri: resolved?.uri || ''
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
return /*#__PURE__*/_jsx(MediaViewNativeComponent, {
|
|
31
|
+
...restProps,
|
|
32
|
+
source: resolvedSource,
|
|
33
|
+
resizeMode: resizeMode
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
export default MediaView;
|
|
37
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["Image","MediaViewNativeComponent","jsx","_jsx","MediaView","props","source","resizeMode","restProps","resolvedSource","uri","resolved","resolveAssetSource"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":";;AACA,SAAkCA,KAAK,QAAQ,cAAc;AAC7D,OAAOC,wBAAwB,MAIxB,4BAA4B;;AAInC;AAAA,SAAAC,GAAA,IAAAC,IAAA;AAYA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,SAASA,CAACC,KAAqB,EAAE;EAC/C,MAAM;IAAEC,MAAM;IAAEC,UAAU,GAAG,SAAS;IAAE,GAAGC;EAAU,CAAC,GAAGH,KAAK;EAE9D,IAAII,cAAgC;EAEpC,IAAI,OAAOH,MAAM,KAAK,QAAQ,IAAI,KAAK,IAAIA,MAAM,EAAE;IACjDG,cAAc,GAAG;MAAEC,GAAG,EAAEJ,MAAM,CAACI;IAAI,CAAC;EACtC,CAAC,MAAM;IACL,MAAMC,QAAQ,GAAGX,KAAK,CAACY,kBAAkB,CAACN,MAA4B,CAAC;IACvEG,cAAc,GAAG;MAAEC,GAAG,EAAEC,QAAQ,EAAED,GAAG,IAAI;IAAG,CAAC;EAC/C;EAEA,oBACEP,IAAA,CAACF,wBAAwB;IAAA,GACnBO,SAAS;IACbF,MAAM,EAAEG,cAAe;IACvBF,UAAU,EAAEA;EAAW,CACxB,CAAC;AAEN;AAEA,eAAeH,SAAS","ignoreList":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"module"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":[],"sourceRoot":"../../src","sources":["react-native.d.ts"],"mappings":"","ignoreList":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"module"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ViewProps } from 'react-native';
|
|
2
|
+
import type { DirectEventHandler } from 'react-native/Libraries/Types/CodegenTypes';
|
|
3
|
+
import type { HostComponent } from 'react-native';
|
|
4
|
+
/**
|
|
5
|
+
* Source props for media (resolved from require())
|
|
6
|
+
*/
|
|
7
|
+
export interface MediaSourceProps {
|
|
8
|
+
/** URI of the image or video */
|
|
9
|
+
uri?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Resize mode for image display (aligned with React Native Image)
|
|
13
|
+
*/
|
|
14
|
+
export type ResizeMode = 'cover' | 'contain' | 'stretch' | 'center';
|
|
15
|
+
/**
|
|
16
|
+
* Native props for MediaView component
|
|
17
|
+
*/
|
|
18
|
+
interface NativeProps extends ViewProps {
|
|
19
|
+
/** Source of the media */
|
|
20
|
+
source?: MediaSourceProps;
|
|
21
|
+
/** Resize mode for image display (aligned with React Native Image) */
|
|
22
|
+
resizeMode?: string;
|
|
23
|
+
/** Callback when loading starts */
|
|
24
|
+
onLoadStart?: DirectEventHandler<null>;
|
|
25
|
+
/** Callback when the image is loaded */
|
|
26
|
+
onLoad?: DirectEventHandler<null>;
|
|
27
|
+
/** Callback when loading ends (success or failure) */
|
|
28
|
+
onLoadEnd?: DirectEventHandler<null>;
|
|
29
|
+
/** Callback when an error occurs */
|
|
30
|
+
onError?: DirectEventHandler<Readonly<{
|
|
31
|
+
error: string;
|
|
32
|
+
}>>;
|
|
33
|
+
}
|
|
34
|
+
export type MediaViewComponent = HostComponent<NativeProps>;
|
|
35
|
+
declare const _default: MediaViewComponent;
|
|
36
|
+
export default _default;
|
|
37
|
+
//# sourceMappingURL=MediaViewNativeComponent.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MediaViewNativeComponent.d.ts","sourceRoot":"","sources":["../../../src/MediaViewNativeComponent.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,2CAA2C,CAAC;AACpF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAElD;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,gCAAgC;IAChC,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,SAAS,GAAG,SAAS,GAAG,QAAQ,CAAC;AAEpE;;GAEG;AACH,UAAU,WAAY,SAAQ,SAAS;IACrC,0BAA0B;IAC1B,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B,sEAAsE;IACtE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,mCAAmC;IACnC,WAAW,CAAC,EAAE,kBAAkB,CAAC,IAAI,CAAC,CAAC;IACvC,wCAAwC;IACxC,MAAM,CAAC,EAAE,kBAAkB,CAAC,IAAI,CAAC,CAAC;IAClC,sDAAsD;IACtD,SAAS,CAAC,EAAE,kBAAkB,CAAC,IAAI,CAAC,CAAC;IACrC,oCAAoC;IACpC,OAAO,CAAC,EAAE,kBAAkB,CAAC,QAAQ,CAAC;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,CAAC;CAC3D;AAED,MAAM,MAAM,kBAAkB,GAAG,aAAa,CAAC,WAAW,CAAC,CAAC;wBAIvD,kBAAkB;AAFvB,wBAEwB"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ComponentProps } from 'react';
|
|
2
|
+
import { type ImageRequireSource } from 'react-native';
|
|
3
|
+
import MediaViewNativeComponent, { type MediaViewComponent, type MediaSourceProps, type ResizeMode } from './MediaViewNativeComponent';
|
|
4
|
+
export type { MediaSourceProps, ResizeMode, MediaViewComponent };
|
|
5
|
+
/** @deprecated Use ResizeMode instead */
|
|
6
|
+
export type ContentMode = ResizeMode;
|
|
7
|
+
type NativeProps = ComponentProps<typeof MediaViewNativeComponent>;
|
|
8
|
+
export interface MediaViewProps extends Omit<NativeProps, 'source'> {
|
|
9
|
+
/** Source of the media - use require('./path/to/file') or { uri: 'https://...' } */
|
|
10
|
+
source: ImageRequireSource | {
|
|
11
|
+
uri: string;
|
|
12
|
+
};
|
|
13
|
+
/** Resize mode for display (aligned with React Native Image) */
|
|
14
|
+
resizeMode?: ResizeMode;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* MediaView - A React Native component for displaying images and videos
|
|
18
|
+
* Supports AVIF, PNG, JPEG, GIF, WebP images, and video files (mp4, webm, mov, etc.)
|
|
19
|
+
* Video URIs are automatically detected by file extension and rendered with autoplay, loop, muted, playsinline.
|
|
20
|
+
*/
|
|
21
|
+
export declare function MediaView(props: MediaViewProps): import("react/jsx-runtime").JSX.Element;
|
|
22
|
+
export default MediaView;
|
|
23
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,OAAO,CAAC;AAC5C,OAAO,EAAE,KAAK,kBAAkB,EAAS,MAAM,cAAc,CAAC;AAC9D,OAAO,wBAAwB,EAAE,EAC/B,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,EACrB,KAAK,UAAU,EAChB,MAAM,4BAA4B,CAAC;AAEpC,YAAY,EAAE,gBAAgB,EAAE,UAAU,EAAE,kBAAkB,EAAE,CAAC;AAEjE,yCAAyC;AACzC,MAAM,MAAM,WAAW,GAAG,UAAU,CAAC;AAErC,KAAK,WAAW,GAAG,cAAc,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAEnE,MAAM,WAAW,cAAe,SAAQ,IAAI,CAAC,WAAW,EAAE,QAAQ,CAAC;IACjE,oFAAoF;IACpF,MAAM,EAAE,kBAAkB,GAAG;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IAC7C,gEAAgE;IAChE,UAAU,CAAC,EAAE,UAAU,CAAC;CACzB;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,2CAmB9C;AAED,eAAe,SAAS,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-native-media-view",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "A React Native component for displaying video and images. Supports iOS only with the new architecture (Fabric).",
|
|
5
|
+
"main": "./lib/module/index.js",
|
|
6
|
+
"types": "./lib/typescript/src/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"source": "./src/index.tsx",
|
|
10
|
+
"types": "./lib/typescript/src/index.d.ts",
|
|
11
|
+
"default": "./lib/module/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"src",
|
|
17
|
+
"lib",
|
|
18
|
+
"android",
|
|
19
|
+
"ios",
|
|
20
|
+
"cpp",
|
|
21
|
+
"*.podspec",
|
|
22
|
+
"react-native.config.js",
|
|
23
|
+
"!ios/build",
|
|
24
|
+
"!android/build",
|
|
25
|
+
"!android/gradle",
|
|
26
|
+
"!android/gradlew",
|
|
27
|
+
"!android/gradlew.bat",
|
|
28
|
+
"!android/local.properties",
|
|
29
|
+
"!**/__tests__",
|
|
30
|
+
"!**/__fixtures__",
|
|
31
|
+
"!**/__mocks__",
|
|
32
|
+
"!**/.*"
|
|
33
|
+
],
|
|
34
|
+
"scripts": {
|
|
35
|
+
"example": "yarn workspace react-native-media-view-example",
|
|
36
|
+
"clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib",
|
|
37
|
+
"prepare": "bob build",
|
|
38
|
+
"typecheck": "tsc",
|
|
39
|
+
"lint": "eslint \"**/*.{js,ts,tsx}\"",
|
|
40
|
+
"release": "release-it --only-version"
|
|
41
|
+
},
|
|
42
|
+
"keywords": [
|
|
43
|
+
"react-native",
|
|
44
|
+
"ios",
|
|
45
|
+
"android"
|
|
46
|
+
],
|
|
47
|
+
"repository": {
|
|
48
|
+
"type": "git",
|
|
49
|
+
"url": "git+https://github.com/luoxuhai/react-native-media-view.git"
|
|
50
|
+
},
|
|
51
|
+
"author": "Darkce <darkce97@gmail.com> (https://github.com/luoxuhai)",
|
|
52
|
+
"license": "MIT",
|
|
53
|
+
"bugs": {
|
|
54
|
+
"url": "https://github.com/luoxuhai/react-native-media-view/issues"
|
|
55
|
+
},
|
|
56
|
+
"homepage": "https://github.com/luoxuhai/react-native-media-view#readme",
|
|
57
|
+
"publishConfig": {
|
|
58
|
+
"registry": "https://registry.npmjs.org/"
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@commitlint/config-conventional": "^19.8.1",
|
|
62
|
+
"@eslint/compat": "^1.3.2",
|
|
63
|
+
"@eslint/eslintrc": "^3.3.1",
|
|
64
|
+
"@eslint/js": "^9.35.0",
|
|
65
|
+
"@react-native/babel-preset": "0.83.0",
|
|
66
|
+
"@react-native/eslint-config": "0.83.0",
|
|
67
|
+
"@release-it/conventional-changelog": "^10.0.1",
|
|
68
|
+
"@types/react": "^19.2.0",
|
|
69
|
+
"commitlint": "^19.8.1",
|
|
70
|
+
"del-cli": "^6.0.0",
|
|
71
|
+
"eslint": "^9.35.0",
|
|
72
|
+
"eslint-config-prettier": "^10.1.8",
|
|
73
|
+
"eslint-plugin-prettier": "^5.5.4",
|
|
74
|
+
"lefthook": "^2.0.3",
|
|
75
|
+
"prettier": "^2.8.8",
|
|
76
|
+
"react": "19.2.0",
|
|
77
|
+
"react-native": "0.83.0",
|
|
78
|
+
"react-native-builder-bob": "^0.40.13",
|
|
79
|
+
"release-it": "^19.0.4",
|
|
80
|
+
"turbo": "^2.5.6",
|
|
81
|
+
"typescript": "^5.9.2"
|
|
82
|
+
},
|
|
83
|
+
"peerDependencies": {
|
|
84
|
+
"react": "*",
|
|
85
|
+
"react-native": "*"
|
|
86
|
+
},
|
|
87
|
+
"workspaces": [
|
|
88
|
+
"example"
|
|
89
|
+
],
|
|
90
|
+
"packageManager": "yarn@4.11.0",
|
|
91
|
+
"react-native-builder-bob": {
|
|
92
|
+
"source": "src",
|
|
93
|
+
"output": "lib",
|
|
94
|
+
"targets": [
|
|
95
|
+
[
|
|
96
|
+
"module",
|
|
97
|
+
{
|
|
98
|
+
"esm": true
|
|
99
|
+
}
|
|
100
|
+
],
|
|
101
|
+
[
|
|
102
|
+
"typescript",
|
|
103
|
+
{
|
|
104
|
+
"project": "tsconfig.build.json"
|
|
105
|
+
}
|
|
106
|
+
]
|
|
107
|
+
]
|
|
108
|
+
},
|
|
109
|
+
"codegenConfig": {
|
|
110
|
+
"name": "MediaViewSpec",
|
|
111
|
+
"type": "all",
|
|
112
|
+
"jsSrcsDir": "src",
|
|
113
|
+
"android": {
|
|
114
|
+
"javaPackageName": "com.mediaview"
|
|
115
|
+
},
|
|
116
|
+
"ios": {
|
|
117
|
+
"components": {
|
|
118
|
+
"MediaView": {
|
|
119
|
+
"className": "MediaView"
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
"prettier": {
|
|
125
|
+
"quoteProps": "consistent",
|
|
126
|
+
"singleQuote": true,
|
|
127
|
+
"tabWidth": 2,
|
|
128
|
+
"trailingComma": "es5",
|
|
129
|
+
"useTabs": false
|
|
130
|
+
},
|
|
131
|
+
"commitlint": {
|
|
132
|
+
"extends": [
|
|
133
|
+
"@commitlint/config-conventional"
|
|
134
|
+
]
|
|
135
|
+
},
|
|
136
|
+
"release-it": {
|
|
137
|
+
"git": {
|
|
138
|
+
"commitMessage": "chore: release ${version}",
|
|
139
|
+
"tagName": "v${version}"
|
|
140
|
+
},
|
|
141
|
+
"npm": {
|
|
142
|
+
"publish": true
|
|
143
|
+
},
|
|
144
|
+
"github": {
|
|
145
|
+
"release": true
|
|
146
|
+
},
|
|
147
|
+
"plugins": {
|
|
148
|
+
"@release-it/conventional-changelog": {
|
|
149
|
+
"preset": {
|
|
150
|
+
"name": "angular"
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
"create-react-native-library": {
|
|
156
|
+
"type": "fabric-view",
|
|
157
|
+
"languages": "kotlin-objc",
|
|
158
|
+
"tools": [
|
|
159
|
+
"eslint",
|
|
160
|
+
"lefthook",
|
|
161
|
+
"release-it"
|
|
162
|
+
],
|
|
163
|
+
"version": "0.57.0"
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = "react-native-media-view"
|
|
7
|
+
s.version = package["version"]
|
|
8
|
+
s.summary = package["description"]
|
|
9
|
+
s.homepage = package["homepage"]
|
|
10
|
+
s.license = package["license"]
|
|
11
|
+
s.authors = package["author"]
|
|
12
|
+
|
|
13
|
+
s.platforms = { :ios => min_ios_version_supported }
|
|
14
|
+
s.source = { :git => "https://github.com/luoxuhai/react-native-media-view.git", :tag => "#{s.version}" }
|
|
15
|
+
|
|
16
|
+
s.source_files = "ios/**/*.{h,m,mm,swift,cpp}"
|
|
17
|
+
s.private_header_files = "ios/**/*.h"
|
|
18
|
+
|
|
19
|
+
install_modules_dependencies(s)
|
|
20
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { codegenNativeComponent } from 'react-native';
|
|
2
|
+
import type { ViewProps } from 'react-native';
|
|
3
|
+
import type { DirectEventHandler } from 'react-native/Libraries/Types/CodegenTypes';
|
|
4
|
+
import type { HostComponent } from 'react-native';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Source props for media (resolved from require())
|
|
8
|
+
*/
|
|
9
|
+
export interface MediaSourceProps {
|
|
10
|
+
/** URI of the image or video */
|
|
11
|
+
uri?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Resize mode for image display (aligned with React Native Image)
|
|
16
|
+
*/
|
|
17
|
+
export type ResizeMode = 'cover' | 'contain' | 'stretch' | 'center';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Native props for MediaView component
|
|
21
|
+
*/
|
|
22
|
+
interface NativeProps extends ViewProps {
|
|
23
|
+
/** Source of the media */
|
|
24
|
+
source?: MediaSourceProps;
|
|
25
|
+
/** Resize mode for image display (aligned with React Native Image) */
|
|
26
|
+
resizeMode?: string;
|
|
27
|
+
/** Callback when loading starts */
|
|
28
|
+
onLoadStart?: DirectEventHandler<null>;
|
|
29
|
+
/** Callback when the image is loaded */
|
|
30
|
+
onLoad?: DirectEventHandler<null>;
|
|
31
|
+
/** Callback when loading ends (success or failure) */
|
|
32
|
+
onLoadEnd?: DirectEventHandler<null>;
|
|
33
|
+
/** Callback when an error occurs */
|
|
34
|
+
onError?: DirectEventHandler<Readonly<{ error: string }>>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type MediaViewComponent = HostComponent<NativeProps>;
|
|
38
|
+
|
|
39
|
+
export default codegenNativeComponent<NativeProps>(
|
|
40
|
+
'MediaView'
|
|
41
|
+
) as MediaViewComponent;
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ComponentProps } from 'react';
|
|
2
|
+
import { type ImageRequireSource, Image } from 'react-native';
|
|
3
|
+
import MediaViewNativeComponent, {
|
|
4
|
+
type MediaViewComponent,
|
|
5
|
+
type MediaSourceProps,
|
|
6
|
+
type ResizeMode,
|
|
7
|
+
} from './MediaViewNativeComponent';
|
|
8
|
+
|
|
9
|
+
export type { MediaSourceProps, ResizeMode, MediaViewComponent };
|
|
10
|
+
|
|
11
|
+
/** @deprecated Use ResizeMode instead */
|
|
12
|
+
export type ContentMode = ResizeMode;
|
|
13
|
+
|
|
14
|
+
type NativeProps = ComponentProps<typeof MediaViewNativeComponent>;
|
|
15
|
+
|
|
16
|
+
export interface MediaViewProps extends Omit<NativeProps, 'source'> {
|
|
17
|
+
/** Source of the media - use require('./path/to/file') or { uri: 'https://...' } */
|
|
18
|
+
source: ImageRequireSource | { uri: string };
|
|
19
|
+
/** Resize mode for display (aligned with React Native Image) */
|
|
20
|
+
resizeMode?: ResizeMode;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* MediaView - A React Native component for displaying images and videos
|
|
25
|
+
* Supports AVIF, PNG, JPEG, GIF, WebP images, and video files (mp4, webm, mov, etc.)
|
|
26
|
+
* Video URIs are automatically detected by file extension and rendered with autoplay, loop, muted, playsinline.
|
|
27
|
+
*/
|
|
28
|
+
export function MediaView(props: MediaViewProps) {
|
|
29
|
+
const { source, resizeMode = 'contain', ...restProps } = props;
|
|
30
|
+
|
|
31
|
+
let resolvedSource: MediaSourceProps;
|
|
32
|
+
|
|
33
|
+
if (typeof source === 'object' && 'uri' in source) {
|
|
34
|
+
resolvedSource = { uri: source.uri };
|
|
35
|
+
} else {
|
|
36
|
+
const resolved = Image.resolveAssetSource(source as ImageRequireSource);
|
|
37
|
+
resolvedSource = { uri: resolved?.uri || '' };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<MediaViewNativeComponent
|
|
42
|
+
{...restProps}
|
|
43
|
+
source={resolvedSource}
|
|
44
|
+
resizeMode={resizeMode}
|
|
45
|
+
/>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export default MediaView;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Type declarations for React Native internal modules
|
|
2
|
+
// These are required for React Native Fabric component codegen
|
|
3
|
+
|
|
4
|
+
declare module 'react-native/Libraries/Types/CodegenTypes' {
|
|
5
|
+
export type Double = number;
|
|
6
|
+
export type Float = number;
|
|
7
|
+
export type Int32 = number;
|
|
8
|
+
export type UnsafeObject = object;
|
|
9
|
+
export type WithDefault<T, _V> = T | _V | undefined;
|
|
10
|
+
export type DirectEventHandler<T> = (event: { nativeEvent: T }) => void;
|
|
11
|
+
export type BubblingEventHandler<T> = (event: { nativeEvent: T }) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
declare module 'react-native/Libraries/Utilities/codegenNativeComponent' {
|
|
15
|
+
import type { HostComponent } from 'react-native';
|
|
16
|
+
|
|
17
|
+
export default function codegenNativeComponent<P>(
|
|
18
|
+
componentName: string,
|
|
19
|
+
options?: {
|
|
20
|
+
interfaceOnly?: boolean;
|
|
21
|
+
paperComponentName?: string;
|
|
22
|
+
excludedPlatforms?: string[];
|
|
23
|
+
}
|
|
24
|
+
): HostComponent<P>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
declare module 'react-native/Libraries/Utilities/codegenNativeCommands' {
|
|
28
|
+
export default function codegenNativeCommands<T>(options: {
|
|
29
|
+
supportedCommands: ReadonlyArray<keyof T>;
|
|
30
|
+
}): T;
|
|
31
|
+
}
|