react-native-alpha-video-gpu 0.0.0-stage → 1.0.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 +21 -0
- package/README.md +949 -2
- package/THIRD_PARTY_NOTICES.md +14 -0
- package/android/src/main/AndroidManifest.xml +9 -0
- package/android/src/main/cpp/CMakeLists.txt +41 -0
- package/android/src/main/cpp/alphavideogpu_jni.cpp +1168 -0
- package/android/src/main/cpp/astc_decomp.cpp +1550 -0
- package/android/src/main/cpp/astc_decomp.h +41 -0
- package/android/src/main/cpp/source_jni.cpp +242 -0
- package/android/src/main/cpp/source_jni.h +13 -0
- package/android/src/main/java/dev/mediacache/alphavideo/gpu/AlphaVideoGpuNative.java +43 -0
- package/android/src/main/java/dev/mediacache/alphavideo/gpu/AlphaVideoGpuReader.java +209 -0
- package/android/src/main/java/dev/mediacache/alphavideo/gpu/AlphaVideoGpuSource.java +328 -0
- package/android/src/main/java/dev/mediacache/alphavideo/gpu/AlphaVideoGpuView.java +2126 -0
- package/android/src/main/java/dev/mediacache/alphavideo/gpu/Fetcher.java +37 -0
- package/android/src/main/java/dev/mediacache/alphavideo/gpu/HttpFetcher.java +156 -0
- package/android/src/main/java/dev/mediacache/alphavideo/gpu/Ktx2Metadata.java +78 -0
- package/android/src/main/java/dev/mediacache/alphavideo/gpu/RangeSourceLink.java +54 -0
- package/android/src/main/java/dev/mediacache/alphavideo/gpu/ZstdSeekTable.java +120 -0
- package/android/src/rangesource/java/dev/mediacache/alphavideo/gpu/AppDataSource.java +50 -0
- package/ios/Sources/AlphaVideoGPU/AlphaVideoGpuSource.h +63 -0
- package/ios/Sources/AlphaVideoGPU/AlphaVideoGpuSource.mm +522 -0
- package/ios/Sources/AlphaVideoGPU/AlphaVideoGpuView.mm +2722 -0
- package/ios/Sources/AlphaVideoGPU/astc_decomp.cpp +1550 -0
- package/ios/Sources/AlphaVideoGPU/astc_decomp.h +41 -0
- package/ios/Sources/AlphaVideoGPU/include/AlphaVideoGPU.h +110 -0
- package/ios/Sources/AlphaVideoGPU/zstd_seek.h +250 -0
- package/package.json +77 -4
- package/react-native/android/build.gradle +92 -0
- package/react-native/android/src/main/AndroidManifest.xml +9 -0
- package/react-native/android/src/main/java/dev/mediacache/alphavideo/gpu/reactnative/AlphaVideoGpuPackage.java +26 -0
- package/react-native/android/src/main/java/dev/mediacache/alphavideo/gpu/reactnative/AlphaVideoGpuViewManager.java +187 -0
- package/react-native/android/src/main/java/dev/mediacache/alphavideo/gpu/reactnative/PlayerEventForwarder.java +107 -0
- package/react-native/android/src/newarch/java/dev/mediacache/alphavideo/gpu/reactnative/AlphaVideoGpuViewManagerSpec.java +23 -0
- package/react-native/android/src/norangesource/java/dev/mediacache/alphavideo/gpu/AppDataSource.java +15 -0
- package/react-native/android/src/oldarch/java/dev/mediacache/alphavideo/gpu/reactnative/AlphaVideoGpuViewManagerSpec.java +56 -0
- package/react-native/ios/RNAlphaVideoGpuViewComponentView.mm +176 -0
- package/react-native/ios/RNAlphaVideoGpuViewManager.mm +175 -0
- package/react-native/src/AlphaVideoGpuView.js +2 -0
- package/react-native/src/AlphaVideoGpuView.web.tsx +264 -0
- package/react-native/src/AlphaVideoGpuViewNativeComponent.ts +128 -0
- package/react-native/src/AlphaVideoGpuViewNativeComponent.web.ts +2 -0
- package/react-native/src/index.d.ts +95 -0
- package/react-native/src/index.js +162 -0
- package/react-native-alpha-video-gpu.podspec +37 -0
- package/react-native.config.js +12 -0
- package/third_party/licenses/Apache-2.0.txt +208 -0
- package/third_party/range-source/LICENSE +21 -0
- package/third_party/range-source/README.md +10 -0
- package/third_party/range-source/include/range_source.h +395 -0
- package/third_party/range-source/include/range_source_download.h +309 -0
- package/third_party/zstd/LICENSE +30 -0
- package/third_party/zstd/zstd.c +38717 -0
- package/third_party/zstd/zstd.h +2450 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
//
|
|
2
|
+
// RNAlphaVideoGpuViewManager.mm
|
|
3
|
+
// React Native view manager for AlphaVideoGpuView (old architecture; the new one uses
|
|
4
|
+
// RNAlphaVideoGpuViewComponentView.mm)
|
|
5
|
+
//
|
|
6
|
+
|
|
7
|
+
#ifndef RCT_NEW_ARCH_ENABLED
|
|
8
|
+
|
|
9
|
+
#import <React/RCTUIManager.h>
|
|
10
|
+
#import <React/RCTViewManager.h>
|
|
11
|
+
#import "AlphaVideoGPU.h"
|
|
12
|
+
|
|
13
|
+
#pragma mark - RNAlphaVideoGpuView
|
|
14
|
+
|
|
15
|
+
/** AlphaVideoGpuView with the React Native props and events. */
|
|
16
|
+
@interface RNAlphaVideoGpuView : AlphaVideoGpuView
|
|
17
|
+
|
|
18
|
+
// `source` prop: { uri, cacheKey, headers?: [name, value, ...], dataSource? }
|
|
19
|
+
@property (nonatomic, copy) NSDictionary *reactSource;
|
|
20
|
+
|
|
21
|
+
@property (nonatomic, copy) RCTDirectEventBlock reactOnLoad;
|
|
22
|
+
@property (nonatomic, copy) RCTDirectEventBlock reactOnFrameUpdate;
|
|
23
|
+
@property (nonatomic, copy) RCTDirectEventBlock reactOnStateChange;
|
|
24
|
+
@property (nonatomic, copy) RCTDirectEventBlock reactOnFinished;
|
|
25
|
+
@property (nonatomic, copy) RCTDirectEventBlock reactOnError;
|
|
26
|
+
|
|
27
|
+
@end
|
|
28
|
+
|
|
29
|
+
@implementation RNAlphaVideoGpuView
|
|
30
|
+
|
|
31
|
+
- (instancetype)initWithFrame:(CGRect)frame
|
|
32
|
+
{
|
|
33
|
+
self = [super initWithFrame:frame];
|
|
34
|
+
if (self) {
|
|
35
|
+
[self forwardEvents];
|
|
36
|
+
}
|
|
37
|
+
return self;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
static NSString *RNAVGString(id value)
|
|
41
|
+
{
|
|
42
|
+
return [value isKindOfClass:[NSString class]] && [value length] > 0 ? value : nil;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
- (void)setReactSource:(NSDictionary *)reactSource
|
|
46
|
+
{
|
|
47
|
+
_reactSource = [reactSource copy];
|
|
48
|
+
// Adopt a cacheKey inside `source` before loading: React gives no ordering guarantee against
|
|
49
|
+
// the separate `cacheKey` prop, and the load reads the key to name what it stores.
|
|
50
|
+
NSString *cacheKey = RNAVGString(reactSource[@"cacheKey"]);
|
|
51
|
+
if (cacheKey) {
|
|
52
|
+
self.cacheKey = cacheKey;
|
|
53
|
+
}
|
|
54
|
+
NSArray *flat = [reactSource[@"headers"] isKindOfClass:[NSArray class]] ? reactSource[@"headers"] : @[];
|
|
55
|
+
NSMutableDictionary<NSString *, NSString *> *headers = [NSMutableDictionary dictionary];
|
|
56
|
+
for (NSUInteger i = 0; i + 1 < flat.count; i += 2) {
|
|
57
|
+
if ([flat[i] isKindOfClass:[NSString class]] && [flat[i + 1] isKindOfClass:[NSString class]]) {
|
|
58
|
+
headers[flat[i]] = flat[i + 1];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
self.headers = headers;
|
|
62
|
+
self.dataSource = RNAVGString(reactSource[@"dataSource"]);
|
|
63
|
+
self.source = RNAVGString(reactSource[@"uri"]);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
- (void)forwardEvents
|
|
67
|
+
{
|
|
68
|
+
__weak RNAlphaVideoGpuView *weakSelf = self;
|
|
69
|
+
self.onLoad = ^(NSInteger width, NSInteger height, NSInteger frameCount, CGFloat frameRate) {
|
|
70
|
+
RCTDirectEventBlock block = weakSelf.reactOnLoad;
|
|
71
|
+
if (!block) return;
|
|
72
|
+
block(@{
|
|
73
|
+
@"width": @(width),
|
|
74
|
+
@"height": @(height),
|
|
75
|
+
@"frameCount": @(frameCount),
|
|
76
|
+
@"frameRate": @(frameRate),
|
|
77
|
+
@"duration": @(frameCount > 0 && frameRate > 0 ? frameCount / frameRate : 0)
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
self.onFrameUpdate = ^(NSInteger frame, NSInteger totalFrames) {
|
|
81
|
+
RCTDirectEventBlock block = weakSelf.reactOnFrameUpdate;
|
|
82
|
+
if (block) block(@{@"frame": @(frame), @"totalFrames": @(totalFrames)});
|
|
83
|
+
};
|
|
84
|
+
self.onStateChange = ^(NSString *state) {
|
|
85
|
+
RCTDirectEventBlock block = weakSelf.reactOnStateChange;
|
|
86
|
+
if (block) block(@{@"state": state});
|
|
87
|
+
};
|
|
88
|
+
self.onFinished = ^{
|
|
89
|
+
RCTDirectEventBlock block = weakSelf.reactOnFinished;
|
|
90
|
+
if (block) block(@{});
|
|
91
|
+
};
|
|
92
|
+
self.onError = ^(NSString *error, NSString *code) {
|
|
93
|
+
RCTDirectEventBlock block = weakSelf.reactOnError;
|
|
94
|
+
if (block) block(code ? @{@"error": error, @"code": code} : @{@"error": error});
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@end
|
|
99
|
+
|
|
100
|
+
#pragma mark - RNAlphaVideoGpuViewManager
|
|
101
|
+
|
|
102
|
+
@interface RNAlphaVideoGpuViewManager : RCTViewManager
|
|
103
|
+
@end
|
|
104
|
+
|
|
105
|
+
@implementation RNAlphaVideoGpuViewManager
|
|
106
|
+
|
|
107
|
+
RCT_EXPORT_MODULE(AlphaVideoGpuView)
|
|
108
|
+
|
|
109
|
+
+ (BOOL)requiresMainQueueSetup
|
|
110
|
+
{
|
|
111
|
+
return YES;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
- (UIView *)view
|
|
115
|
+
{
|
|
116
|
+
return [[RNAlphaVideoGpuView alloc] initWithFrame:CGRectZero];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
RCT_REMAP_VIEW_PROPERTY(source, reactSource, NSDictionary)
|
|
120
|
+
RCT_REMAP_VIEW_PROPERTY(paused, playbackPaused, BOOL)
|
|
121
|
+
RCT_EXPORT_VIEW_PROPERTY(cacheKey, NSString)
|
|
122
|
+
RCT_EXPORT_VIEW_PROPERTY(storage, NSString)
|
|
123
|
+
RCT_EXPORT_VIEW_PROPERTY(maxStorageBytes, int64_t)
|
|
124
|
+
RCT_EXPORT_VIEW_PROPERTY(autoPlay, BOOL)
|
|
125
|
+
RCT_EXPORT_VIEW_PROPERTY(loop, BOOL)
|
|
126
|
+
RCT_EXPORT_VIEW_PROPERTY(playbackSpeed, CGFloat)
|
|
127
|
+
RCT_EXPORT_VIEW_PROPERTY(enableFrameEvents, BOOL)
|
|
128
|
+
RCT_EXPORT_VIEW_PROPERTY(streamingPrefetchAhead, NSInteger)
|
|
129
|
+
RCT_EXPORT_VIEW_PROPERTY(memoryStrategy, NSString)
|
|
130
|
+
RCT_EXPORT_VIEW_PROPERTY(stream, BOOL)
|
|
131
|
+
RCT_EXPORT_VIEW_PROPERTY(verbose, BOOL)
|
|
132
|
+
RCT_REMAP_VIEW_PROPERTY(onLoad, reactOnLoad, RCTDirectEventBlock)
|
|
133
|
+
RCT_REMAP_VIEW_PROPERTY(onFrameUpdate, reactOnFrameUpdate, RCTDirectEventBlock)
|
|
134
|
+
RCT_REMAP_VIEW_PROPERTY(onStateChange, reactOnStateChange, RCTDirectEventBlock)
|
|
135
|
+
RCT_REMAP_VIEW_PROPERTY(onFinished, reactOnFinished, RCTDirectEventBlock)
|
|
136
|
+
RCT_REMAP_VIEW_PROPERTY(onError, reactOnError, RCTDirectEventBlock)
|
|
137
|
+
|
|
138
|
+
// The block's view registry is empty under the new architecture's interop layer, so look the
|
|
139
|
+
// view up by tag instead.
|
|
140
|
+
- (void)withView:(NSNumber *)reactTag run:(void (^)(AlphaVideoGpuView *view))body
|
|
141
|
+
{
|
|
142
|
+
[self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
|
|
143
|
+
UIView *view = [uiManager viewForReactTag:reactTag];
|
|
144
|
+
if ([view isKindOfClass:[AlphaVideoGpuView class]]) body((AlphaVideoGpuView *)view);
|
|
145
|
+
}];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
RCT_EXPORT_METHOD(play:(nonnull NSNumber *)reactTag)
|
|
149
|
+
{
|
|
150
|
+
[self withView:reactTag run:^(AlphaVideoGpuView *view) { [view play]; }];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
RCT_EXPORT_METHOD(pause:(nonnull NSNumber *)reactTag)
|
|
154
|
+
{
|
|
155
|
+
[self withView:reactTag run:^(AlphaVideoGpuView *view) { [view pause]; }];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
RCT_EXPORT_METHOD(stop:(nonnull NSNumber *)reactTag)
|
|
159
|
+
{
|
|
160
|
+
[self withView:reactTag run:^(AlphaVideoGpuView *view) { [view stop]; }];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
RCT_EXPORT_METHOD(seekToFrame:(nonnull NSNumber *)reactTag frame:(nonnull NSNumber *)frame)
|
|
164
|
+
{
|
|
165
|
+
[self withView:reactTag run:^(AlphaVideoGpuView *view) { [view seekToFrame:frame.integerValue]; }];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
RCT_EXPORT_METHOD(seekToTime:(nonnull NSNumber *)reactTag seconds:(nonnull NSNumber *)seconds)
|
|
169
|
+
{
|
|
170
|
+
[self withView:reactTag run:^(AlphaVideoGpuView *view) { [view seekToTime:seconds.doubleValue]; }];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
@end
|
|
174
|
+
|
|
175
|
+
#endif
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AlphaVideoGpuView - React Native Web Implementation
|
|
3
|
+
*
|
|
4
|
+
* GPU-accelerated transparent alpha video player.
|
|
5
|
+
* Thin wrapper around AlphaVideoGpuCore (alpha-video-gpu-web) for React Native Web.
|
|
6
|
+
* Same API as native iOS/Android implementations.
|
|
7
|
+
*
|
|
8
|
+
* The zstd decoder (JS + WASM) comes bundled in alpha-video-gpu-web.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import React, { forwardRef, useImperativeHandle, useRef, useEffect, useMemo } from 'react';
|
|
12
|
+
import { View, StyleSheet } from 'react-native';
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
AlphaVideoGpuCore,
|
|
16
|
+
ZSTDDecoder,
|
|
17
|
+
type AlphaVideoGpuError,
|
|
18
|
+
type AlphaVideoGpuErrorCode,
|
|
19
|
+
type AlphaVideoGpuLoadEvent,
|
|
20
|
+
type ByteSource,
|
|
21
|
+
} from 'alpha-video-gpu-web';
|
|
22
|
+
|
|
23
|
+
export type { AlphaVideoGpuErrorCode, AlphaVideoGpuLoadEvent, PlayerState, ByteSource } from 'alpha-video-gpu-web';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Where to load from (on web, `asset://` and http(s) URLs apply):
|
|
27
|
+
* - `https://…` / `http://…`: read by range as it plays, and kept as `storage` says (on web in the
|
|
28
|
+
* browser's storage, OPFS, else IndexedDB)
|
|
29
|
+
* - `file:///abs/path` or `/abs/path`: read in place
|
|
30
|
+
* - `asset://relative/path`: Android APK `assets/`, the iOS app bundle, or the site root on web
|
|
31
|
+
* - Android only: `content://…`, or a res/raw name (what `require()` resolves to in a release build)
|
|
32
|
+
*
|
|
33
|
+
* On web, `source` can also be a Blob or File, or a ByteSource that reads by offset.
|
|
34
|
+
*/
|
|
35
|
+
export interface SourceType {
|
|
36
|
+
uri?: string;
|
|
37
|
+
/** What the store keeps the source under, for URLs that change while the file doesn't (presigned URLs) */
|
|
38
|
+
cacheKey?: string;
|
|
39
|
+
/** Sent with every request for the URL, e.g. Authorization; given to a `dataSource` too */
|
|
40
|
+
headers?: Record<string, string>;
|
|
41
|
+
/** iOS and Android: a data source registered in native code under this name */
|
|
42
|
+
dataSource?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface AlphaVideoGpuViewProps {
|
|
46
|
+
source?: SourceType | string | Blob | ByteSource | null;
|
|
47
|
+
autoPlay?: boolean;
|
|
48
|
+
loop?: boolean;
|
|
49
|
+
playbackSpeed?: number;
|
|
50
|
+
/** Pause playback and decompression (useful for screen focus/blur) */
|
|
51
|
+
paused?: boolean;
|
|
52
|
+
cacheKey?: string;
|
|
53
|
+
/** Where a URL's bytes are kept between plays; on web 'disk' is the browser's storage. Read on mount */
|
|
54
|
+
storage?: 'auto' | 'disk' | 'memory' | 'none';
|
|
55
|
+
/** The store's cap in bytes (default 512 MB). Read on mount */
|
|
56
|
+
maxStorageBytes?: number;
|
|
57
|
+
style?: any;
|
|
58
|
+
testID?: string;
|
|
59
|
+
// Events have the native shape, so the same handlers work on every platform
|
|
60
|
+
/** Called when file metadata is loaded, provides dimensions and frame info */
|
|
61
|
+
onLoad?: (event: { nativeEvent: AlphaVideoGpuLoadEvent }) => void;
|
|
62
|
+
onStateChange?: (event: { nativeEvent: { state: string } }) => void;
|
|
63
|
+
onFrameUpdate?: (event: { nativeEvent: { frame: number; totalFrames: number } }) => void;
|
|
64
|
+
onFinished?: (event: { nativeEvent: {} }) => void;
|
|
65
|
+
/** `code` is set for the errors a page may want to handle, such as 'ASTC_UNSUPPORTED' */
|
|
66
|
+
onError?: (event: { nativeEvent: { error: string; code?: AlphaVideoGpuErrorCode } }) => void;
|
|
67
|
+
/** Send onFrameUpdate for every frame played; seeks always send it. Default: false */
|
|
68
|
+
enableFrameEvents?: boolean;
|
|
69
|
+
|
|
70
|
+
// Streaming parameters (with stream)
|
|
71
|
+
/** Frames decoded ahead of the shown one, all streaming keeps. Default: 8 */
|
|
72
|
+
streamingPrefetchAhead?: number;
|
|
73
|
+
/** Memory strategy: 'vram' | 'ram'. Default: 'vram' */
|
|
74
|
+
memoryStrategy?: 'vram' | 'ram';
|
|
75
|
+
/** Enable streaming mode for texture array playback. Default: true */
|
|
76
|
+
stream?: boolean;
|
|
77
|
+
/** Log info traces to the console. Default: false */
|
|
78
|
+
verbose?: boolean;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ============================================================================
|
|
82
|
+
// AlphaVideoGpuView Component — Thin React Native Web wrapper
|
|
83
|
+
// ============================================================================
|
|
84
|
+
|
|
85
|
+
export interface AlphaVideoGpuViewRef {
|
|
86
|
+
play: () => void;
|
|
87
|
+
pause: () => void;
|
|
88
|
+
stop: () => void;
|
|
89
|
+
seekToFrame: (frame: number) => void;
|
|
90
|
+
seekToTime: (seconds: number) => void;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export const AlphaVideoGpuView = forwardRef<AlphaVideoGpuViewRef, AlphaVideoGpuViewProps>(({
|
|
94
|
+
source,
|
|
95
|
+
autoPlay = true,
|
|
96
|
+
loop = true,
|
|
97
|
+
playbackSpeed = 1.0,
|
|
98
|
+
paused = false,
|
|
99
|
+
cacheKey,
|
|
100
|
+
storage,
|
|
101
|
+
maxStorageBytes,
|
|
102
|
+
style,
|
|
103
|
+
testID,
|
|
104
|
+
onLoad,
|
|
105
|
+
onStateChange,
|
|
106
|
+
onFrameUpdate,
|
|
107
|
+
onFinished,
|
|
108
|
+
onError,
|
|
109
|
+
streamingPrefetchAhead = 8,
|
|
110
|
+
memoryStrategy = 'vram',
|
|
111
|
+
stream = true,
|
|
112
|
+
enableFrameEvents = false,
|
|
113
|
+
verbose = false,
|
|
114
|
+
}, ref) => {
|
|
115
|
+
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
116
|
+
const coreRef = useRef<AlphaVideoGpuCore | null>(null);
|
|
117
|
+
const loadedSourceRef = useRef<unknown>(null);
|
|
118
|
+
const callbacksRef = useRef({ onLoad, onStateChange, onFrameUpdate, onFinished, onError });
|
|
119
|
+
callbacksRef.current = { onLoad, onStateChange, onFrameUpdate, onFinished, onError };
|
|
120
|
+
|
|
121
|
+
useImperativeHandle(ref, () => ({
|
|
122
|
+
play: () => coreRef.current?.play(),
|
|
123
|
+
pause: () => coreRef.current?.pause(),
|
|
124
|
+
stop: () => coreRef.current?.stop(),
|
|
125
|
+
seekToFrame: (frame: number) => coreRef.current?.seekToFrame(frame),
|
|
126
|
+
seekToTime: (seconds: number) => coreRef.current?.seekToTime(seconds),
|
|
127
|
+
}), []);
|
|
128
|
+
|
|
129
|
+
// A Blob or ByteSource plays as it is; anything else is normalized to { uri } format
|
|
130
|
+
const directSource = isDirectSource(source) ? source : null;
|
|
131
|
+
const normalizedSource = useMemo((): SourceType | null => {
|
|
132
|
+
if (!source || isDirectSource(source)) return null;
|
|
133
|
+
if (typeof source === 'string') return { uri: source };
|
|
134
|
+
return source;
|
|
135
|
+
}, [source]);
|
|
136
|
+
const headersRef = useRef(normalizedSource?.headers);
|
|
137
|
+
headersRef.current = normalizedSource?.headers;
|
|
138
|
+
|
|
139
|
+
// Create core instance when canvas is available
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
const canvas = canvasRef.current;
|
|
142
|
+
if (!canvas || coreRef.current) return;
|
|
143
|
+
|
|
144
|
+
coreRef.current = new AlphaVideoGpuCore(canvas, {
|
|
145
|
+
autoPlay,
|
|
146
|
+
loop,
|
|
147
|
+
playbackSpeed,
|
|
148
|
+
stream,
|
|
149
|
+
streamingPrefetchAhead,
|
|
150
|
+
memoryStrategy,
|
|
151
|
+
verbose,
|
|
152
|
+
frameEvents: enableFrameEvents,
|
|
153
|
+
storage: storage === 'disk' ? 'auto' : storage,
|
|
154
|
+
maxStorageBytes: maxStorageBytes && maxStorageBytes > 0 ? maxStorageBytes : undefined,
|
|
155
|
+
ZSTDDecoderClass: ZSTDDecoder,
|
|
156
|
+
onStateChange: state => callbacksRef.current.onStateChange?.({ nativeEvent: { state } }),
|
|
157
|
+
onLoad: event => callbacksRef.current.onLoad?.({ nativeEvent: event }),
|
|
158
|
+
onFrameUpdate: (frame, totalFrames) =>
|
|
159
|
+
callbacksRef.current.onFrameUpdate?.({ nativeEvent: { frame, totalFrames } }),
|
|
160
|
+
onFinished: () => callbacksRef.current.onFinished?.({ nativeEvent: {} }),
|
|
161
|
+
onError: error => {
|
|
162
|
+
const { code } = error as AlphaVideoGpuError;
|
|
163
|
+
callbacksRef.current.onError?.({ nativeEvent: code ? { error: error.message, code } : { error: error.message } });
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
return () => {
|
|
168
|
+
coreRef.current?.destroy();
|
|
169
|
+
coreRef.current = null;
|
|
170
|
+
loadedSourceRef.current = null;
|
|
171
|
+
};
|
|
172
|
+
// Only create/destroy on mount/unmount
|
|
173
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
174
|
+
}, []);
|
|
175
|
+
|
|
176
|
+
// What identifies "the same video". A presigned URL is re-signed every time the app
|
|
177
|
+
// lists its media, so the URL changes while still addressing the same object; keying
|
|
178
|
+
// the load on it would resetSource() and refetch on each refresh, visibly restarting
|
|
179
|
+
// a loop that should run until unmount. The cache key is the stable identity.
|
|
180
|
+
const sourceCacheKey = normalizedSource?.cacheKey || cacheKey;
|
|
181
|
+
const sourceIdentity = directSource || sourceCacheKey || normalizedSource?.uri || null;
|
|
182
|
+
|
|
183
|
+
// Load source when it changes
|
|
184
|
+
useEffect(() => {
|
|
185
|
+
const core = coreRef.current;
|
|
186
|
+
const toLoad = directSource || normalizedSource?.uri;
|
|
187
|
+
if (!core || !toLoad || !sourceIdentity || loadedSourceRef.current === sourceIdentity) return;
|
|
188
|
+
|
|
189
|
+
loadedSourceRef.current = sourceIdentity;
|
|
190
|
+
core.setSource(toLoad, { cacheKey: sourceCacheKey, headers: headersRef.current });
|
|
191
|
+
|
|
192
|
+
return () => {
|
|
193
|
+
loadedSourceRef.current = null;
|
|
194
|
+
core.resetSource();
|
|
195
|
+
};
|
|
196
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
197
|
+
}, [sourceIdentity]);
|
|
198
|
+
|
|
199
|
+
useEffect(() => {
|
|
200
|
+
if (coreRef.current) {
|
|
201
|
+
coreRef.current.verbose = verbose;
|
|
202
|
+
}
|
|
203
|
+
}, [verbose]);
|
|
204
|
+
|
|
205
|
+
useEffect(() => {
|
|
206
|
+
if (coreRef.current) {
|
|
207
|
+
coreRef.current.frameEvents = enableFrameEvents;
|
|
208
|
+
}
|
|
209
|
+
}, [enableFrameEvents]);
|
|
210
|
+
|
|
211
|
+
// Handle playback speed changes
|
|
212
|
+
useEffect(() => {
|
|
213
|
+
if (coreRef.current) {
|
|
214
|
+
coreRef.current.playbackSpeed = playbackSpeed;
|
|
215
|
+
}
|
|
216
|
+
}, [playbackSpeed]);
|
|
217
|
+
|
|
218
|
+
// Handle loop changes
|
|
219
|
+
useEffect(() => {
|
|
220
|
+
if (coreRef.current) {
|
|
221
|
+
coreRef.current.loop = loop;
|
|
222
|
+
}
|
|
223
|
+
}, [loop]);
|
|
224
|
+
|
|
225
|
+
// Handle paused prop changes
|
|
226
|
+
useEffect(() => {
|
|
227
|
+
coreRef.current?.setPaused(paused);
|
|
228
|
+
}, [paused]);
|
|
229
|
+
|
|
230
|
+
// Get canvas dimensions from style
|
|
231
|
+
const canvasStyle = useMemo(() => {
|
|
232
|
+
const flatStyle = StyleSheet.flatten(style) || {};
|
|
233
|
+
return {
|
|
234
|
+
width: (flatStyle as any).width || 300,
|
|
235
|
+
height: (flatStyle as any).height || 300,
|
|
236
|
+
};
|
|
237
|
+
}, [style]);
|
|
238
|
+
|
|
239
|
+
return (
|
|
240
|
+
<View testID={testID} style={[styles.container, style]}>
|
|
241
|
+
<canvas
|
|
242
|
+
ref={canvasRef}
|
|
243
|
+
width={canvasStyle.width}
|
|
244
|
+
height={canvasStyle.height}
|
|
245
|
+
style={{ width: '100%', height: '100%' }}
|
|
246
|
+
/>
|
|
247
|
+
</View>
|
|
248
|
+
);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
function isDirectSource(source: AlphaVideoGpuViewProps['source']): source is Blob | ByteSource {
|
|
252
|
+
if (!source || typeof source !== 'object') return false;
|
|
253
|
+
return (typeof Blob !== 'undefined' && source instanceof Blob) || typeof (source as ByteSource).read === 'function';
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const styles = StyleSheet.create({
|
|
257
|
+
container: {
|
|
258
|
+
overflow: 'hidden',
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
AlphaVideoGpuView.displayName = 'AlphaVideoGpuView';
|
|
263
|
+
|
|
264
|
+
export default AlphaVideoGpuView;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type * as React from 'react';
|
|
2
|
+
import type { HostComponent, ViewProps } from 'react-native';
|
|
3
|
+
import type {
|
|
4
|
+
DirectEventHandler,
|
|
5
|
+
Double,
|
|
6
|
+
Float,
|
|
7
|
+
Int32,
|
|
8
|
+
WithDefault,
|
|
9
|
+
} from 'react-native/Libraries/Types/CodegenTypes';
|
|
10
|
+
import codegenNativeCommands from 'react-native/Libraries/Utilities/codegenNativeCommands';
|
|
11
|
+
import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Where to load from:
|
|
15
|
+
* - `https://…` / `http://…`: with react-native-range-source read by range as it plays, and kept
|
|
16
|
+
* as `storage` says; without it downloaded whole
|
|
17
|
+
* - `file:///abs/path` or `/abs/path`: read in place
|
|
18
|
+
* - `asset://relative/path`: Android APK `assets/`, the iOS app bundle, or the site root on web
|
|
19
|
+
* - Android only: `content://…`, or a res/raw name (what `require()` resolves to in a release build)
|
|
20
|
+
* - with `dataSource`, any URI that data source reads
|
|
21
|
+
*/
|
|
22
|
+
export interface SourceType {
|
|
23
|
+
uri?: string;
|
|
24
|
+
/** What the store keeps the source under, for URLs that change while the file doesn't (presigned URLs) */
|
|
25
|
+
cacheKey?: string;
|
|
26
|
+
/** Sent with every request for the URL, e.g. Authorization; given to a `dataSource` too */
|
|
27
|
+
headers?: Record<string, string>;
|
|
28
|
+
/** A data source registered in native code under this name (react-native-range-source's RangeDataSources) */
|
|
29
|
+
dataSource?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface AlphaVideoGpuLoadEvent {
|
|
33
|
+
width: number;
|
|
34
|
+
height: number;
|
|
35
|
+
frameCount: number;
|
|
36
|
+
frameRate: number;
|
|
37
|
+
duration: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type NativeSource = Readonly<{
|
|
41
|
+
uri?: string;
|
|
42
|
+
cacheKey?: string;
|
|
43
|
+
/** Request headers as name, value, name, value... (codegen has no string maps) */
|
|
44
|
+
headers?: ReadonlyArray<string>;
|
|
45
|
+
dataSource?: string;
|
|
46
|
+
}>;
|
|
47
|
+
|
|
48
|
+
type LoadEvent = Readonly<{
|
|
49
|
+
width: Int32;
|
|
50
|
+
height: Int32;
|
|
51
|
+
frameCount: Int32;
|
|
52
|
+
frameRate: Double;
|
|
53
|
+
duration: Double;
|
|
54
|
+
}>;
|
|
55
|
+
|
|
56
|
+
type FrameUpdateEvent = Readonly<{
|
|
57
|
+
frame: Int32;
|
|
58
|
+
totalFrames: Int32;
|
|
59
|
+
}>;
|
|
60
|
+
|
|
61
|
+
type StateChangeEvent = Readonly<{
|
|
62
|
+
state: string;
|
|
63
|
+
}>;
|
|
64
|
+
|
|
65
|
+
type ErrorEvent = Readonly<{
|
|
66
|
+
error: string;
|
|
67
|
+
/** `'NO_SOURCE_MODULE'` for a data source without the range-source add-on */
|
|
68
|
+
code?: string;
|
|
69
|
+
}>;
|
|
70
|
+
|
|
71
|
+
export interface NativeProps extends ViewProps {
|
|
72
|
+
source?: NativeSource;
|
|
73
|
+
autoPlay?: WithDefault<boolean, true>;
|
|
74
|
+
loop?: WithDefault<boolean, true>;
|
|
75
|
+
playbackSpeed?: WithDefault<Float, 1.0>;
|
|
76
|
+
/** Pause playback and decompression (useful for screen focus/blur) */
|
|
77
|
+
paused?: WithDefault<boolean, false>;
|
|
78
|
+
cacheKey?: string;
|
|
79
|
+
/** Where a URL's or data source's bytes are kept between plays */
|
|
80
|
+
storage?: WithDefault<'auto' | 'disk' | 'memory' | 'none', 'auto'>;
|
|
81
|
+
/** The store's cap in bytes; 0 for the default (512 MB) */
|
|
82
|
+
maxStorageBytes?: WithDefault<Double, 0>;
|
|
83
|
+
|
|
84
|
+
// Streaming parameters (with stream)
|
|
85
|
+
/** Frames decoded ahead of the shown one, all streaming keeps: the GPU ring (vram) or the CPU cache (ram). Default: 8 */
|
|
86
|
+
streamingPrefetchAhead?: WithDefault<Int32, 8>;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Memory strategy for frame storage.
|
|
90
|
+
* - 'vram' (default): Store frames in GPU VRAM (texture array). Best for many simultaneous players.
|
|
91
|
+
* - 'ram': Store decoded frames in RAM, upload per-frame. Lower VRAM usage.
|
|
92
|
+
*/
|
|
93
|
+
memoryStrategy?: WithDefault<'vram' | 'ram', 'vram'>;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Enable streaming mode for texture array playback.
|
|
97
|
+
* When true (default): Use streaming decompression with ring buffer (constant memory usage).
|
|
98
|
+
* When false: Load all frames at once.
|
|
99
|
+
*/
|
|
100
|
+
stream?: WithDefault<boolean, true>;
|
|
101
|
+
|
|
102
|
+
/** Send onFrameUpdate for every rendered frame. Default: false */
|
|
103
|
+
enableFrameEvents?: WithDefault<boolean, false>;
|
|
104
|
+
/** Log info traces (Logcat / Xcode console). Warnings and errors always log. Default: false */
|
|
105
|
+
verbose?: WithDefault<boolean, false>;
|
|
106
|
+
|
|
107
|
+
onLoad?: DirectEventHandler<LoadEvent>;
|
|
108
|
+
onFrameUpdate?: DirectEventHandler<FrameUpdateEvent>;
|
|
109
|
+
onStateChange?: DirectEventHandler<StateChangeEvent>;
|
|
110
|
+
onFinished?: DirectEventHandler<Readonly<{}>>;
|
|
111
|
+
onError?: DirectEventHandler<ErrorEvent>;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
type NativeComponentType = HostComponent<NativeProps>;
|
|
115
|
+
|
|
116
|
+
interface NativeCommands {
|
|
117
|
+
play: (viewRef: React.ElementRef<NativeComponentType>) => void;
|
|
118
|
+
pause: (viewRef: React.ElementRef<NativeComponentType>) => void;
|
|
119
|
+
stop: (viewRef: React.ElementRef<NativeComponentType>) => void;
|
|
120
|
+
seekToFrame: (viewRef: React.ElementRef<NativeComponentType>, frame: Int32) => void;
|
|
121
|
+
seekToTime: (viewRef: React.ElementRef<NativeComponentType>, seconds: Double) => void;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export const Commands: NativeCommands = codegenNativeCommands<NativeCommands>({
|
|
125
|
+
supportedCommands: ['play', 'pause', 'stop', 'seekToFrame', 'seekToTime'],
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
export default codegenNativeComponent<NativeProps>('AlphaVideoGpuView') as NativeComponentType;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { ViewStyle } from 'react-native';
|
|
2
|
+
import type React from 'react';
|
|
3
|
+
|
|
4
|
+
// Re-export native component types
|
|
5
|
+
export * from './AlphaVideoGpuViewNativeComponent';
|
|
6
|
+
|
|
7
|
+
// Source type for the wrapper component (see SourceType for the URI forms, headers and dataSource)
|
|
8
|
+
export type AlphaVideoGpuSource = import('./AlphaVideoGpuViewNativeComponent').SourceType;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Where a URL's or data source's bytes are kept between plays, read back by range: 'disk' (the
|
|
12
|
+
* cache folder; the browser's storage on web), 'memory' (for the app's lifetime) or 'none'. 'auto',
|
|
13
|
+
* the default, is 'disk' for URLs and 'none' for a data source, whose caching is its own.
|
|
14
|
+
*/
|
|
15
|
+
export type AlphaVideoGpuStorage = 'auto' | 'disk' | 'memory' | 'none';
|
|
16
|
+
|
|
17
|
+
/** Web only: reads the file by offset (an app's own fetcher or cache); the smallest is `{ size, read }`. */
|
|
18
|
+
export interface AlphaVideoGpuByteSource {
|
|
19
|
+
open?(signal?: AbortSignal): Promise<{ size: number; etag?: string; lastModified?: string }>;
|
|
20
|
+
readonly size?: number;
|
|
21
|
+
read(offset: number, length: number, signal?: AbortSignal): Promise<Uint8Array>;
|
|
22
|
+
close?(): void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Props for the main AlphaVideoGpuView wrapper component (index.js)
|
|
26
|
+
export interface AlphaVideoGpuViewProps {
|
|
27
|
+
/** Source object, or a bare URI string (legacy). On web also a Blob or a byte source */
|
|
28
|
+
source?: AlphaVideoGpuSource | string | Blob | AlphaVideoGpuByteSource | null;
|
|
29
|
+
/** Start playing automatically (default: true) */
|
|
30
|
+
autoPlay?: boolean;
|
|
31
|
+
/** Loop the animation (default: true) */
|
|
32
|
+
loop?: boolean;
|
|
33
|
+
/** Playback speed multiplier (default: 1.0) */
|
|
34
|
+
playbackSpeed?: number;
|
|
35
|
+
/** What the store keeps the source under, instead of its URL (same as `source.cacheKey`) */
|
|
36
|
+
cacheKey?: string;
|
|
37
|
+
/** Where a URL's or data source's bytes are kept between plays. Default: 'auto' */
|
|
38
|
+
storage?: AlphaVideoGpuStorage;
|
|
39
|
+
/** The store's cap in bytes, over every clip, least recently played out first. Default: 512 MB */
|
|
40
|
+
maxStorageBytes?: number;
|
|
41
|
+
/** Pause playback */
|
|
42
|
+
paused?: boolean;
|
|
43
|
+
/** View style */
|
|
44
|
+
style?: ViewStyle;
|
|
45
|
+
/** Locates the view in end-to-end tests */
|
|
46
|
+
testID?: string;
|
|
47
|
+
// Every event has this shape on Android, iOS and web
|
|
48
|
+
/** Callback when player state changes */
|
|
49
|
+
onStateChange?: (event: { nativeEvent: { state: string } }) => void;
|
|
50
|
+
/** Callback after a seek, and for every frame played with `enableFrameEvents` */
|
|
51
|
+
onFrameUpdate?: (event: { nativeEvent: { frame: number; totalFrames: number } }) => void;
|
|
52
|
+
/** Callback when playback finishes */
|
|
53
|
+
onFinished?: (event: { nativeEvent: {} }) => void;
|
|
54
|
+
/**
|
|
55
|
+
* Callback on error. `code` tells apart the errors an app may handle: on the web
|
|
56
|
+
* 'ASTC_UNSUPPORTED' or 'WEBGL_UNSUPPORTED', on iOS and Android 'NO_SOURCE_MODULE' (a data
|
|
57
|
+
* source without the range-source add-on)
|
|
58
|
+
*/
|
|
59
|
+
onError?: (event: { nativeEvent: { error: string; code?: 'ASTC_UNSUPPORTED' | 'WEBGL_UNSUPPORTED' | 'NO_SOURCE_MODULE' | '' } }) => void;
|
|
60
|
+
/** Callback when loaded */
|
|
61
|
+
onLoad?: (event: { nativeEvent: { width: number; height: number; frameCount: number; frameRate: number; duration: number } }) => void;
|
|
62
|
+
|
|
63
|
+
// Streaming parameters
|
|
64
|
+
/** Frames decoded ahead of the shown one, all streaming keeps (in the GPU ring with 'vram', in RAM with 'ram') */
|
|
65
|
+
streamingPrefetchAhead?: number;
|
|
66
|
+
/** Where frames are kept: 'vram' (default) or 'ram' */
|
|
67
|
+
memoryStrategy?: 'vram' | 'ram';
|
|
68
|
+
/** Stream texture-array files instead of loading every frame. Default: true */
|
|
69
|
+
stream?: boolean;
|
|
70
|
+
/** Send onFrameUpdate for every frame played; seeks always send it. Default: false */
|
|
71
|
+
enableFrameEvents?: boolean;
|
|
72
|
+
/** Log info traces. Warnings and errors always log. Default: false */
|
|
73
|
+
verbose?: boolean;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Seeks show the frame (clamped to the clip) and keep playing, or stay paused. They are ignored
|
|
78
|
+
* until `onLoad`. With `stream`, a seek outside the buffered frames decodes the file again from
|
|
79
|
+
* the start (or onwards from the decoder, for a seek ahead of it).
|
|
80
|
+
*/
|
|
81
|
+
export interface AlphaVideoGpuViewRef {
|
|
82
|
+
/** Plays from the shown frame, or from the start once a clip that doesn't loop has finished. */
|
|
83
|
+
play: () => void;
|
|
84
|
+
pause: () => void;
|
|
85
|
+
/** Pauses on the first frame. */
|
|
86
|
+
stop: () => void;
|
|
87
|
+
seekToFrame: (frame: number) => void;
|
|
88
|
+
/** Seeks to the frame showing at that time, in seconds. */
|
|
89
|
+
seekToTime: (seconds: number) => void;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// The actual component is defined in index.js, but we declare the type here
|
|
93
|
+
export declare const AlphaVideoGpuView: React.MemoExoticComponent<
|
|
94
|
+
React.ForwardRefExoticComponent<AlphaVideoGpuViewProps & React.RefAttributes<AlphaVideoGpuViewRef>>
|
|
95
|
+
>;
|