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,2126 @@
|
|
|
1
|
+
package dev.mediacache.alphavideo.gpu;
|
|
2
|
+
|
|
3
|
+
import android.content.Context;
|
|
4
|
+
import android.content.res.AssetFileDescriptor;
|
|
5
|
+
import android.graphics.SurfaceTexture;
|
|
6
|
+
import android.net.Uri;
|
|
7
|
+
import android.opengl.EGL14;
|
|
8
|
+
import android.opengl.EGLConfig;
|
|
9
|
+
import android.opengl.EGLContext;
|
|
10
|
+
import android.opengl.EGLDisplay;
|
|
11
|
+
import android.opengl.EGLSurface;
|
|
12
|
+
import android.opengl.GLES30;
|
|
13
|
+
import android.os.Handler;
|
|
14
|
+
import android.os.Looper;
|
|
15
|
+
import android.util.AttributeSet;
|
|
16
|
+
import android.util.Log;
|
|
17
|
+
import android.view.TextureView;
|
|
18
|
+
|
|
19
|
+
import androidx.annotation.Nullable;
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
import java.io.File;
|
|
23
|
+
import java.util.Arrays;
|
|
24
|
+
import java.util.Collections;
|
|
25
|
+
import java.util.LinkedHashMap;
|
|
26
|
+
import java.util.Map;
|
|
27
|
+
import java.util.Objects;
|
|
28
|
+
import java.io.IOException;
|
|
29
|
+
import java.nio.ByteBuffer;
|
|
30
|
+
import java.nio.ByteOrder;
|
|
31
|
+
import java.nio.FloatBuffer;
|
|
32
|
+
import java.util.concurrent.ExecutorService;
|
|
33
|
+
import java.util.concurrent.Executors;
|
|
34
|
+
import java.util.concurrent.atomic.AtomicBoolean;
|
|
35
|
+
import java.util.concurrent.atomic.AtomicInteger;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Plays transparent animations stored as ASTC KTX2 texture arrays, one frame per layer, drawn
|
|
39
|
+
* with OpenGL ES 3.0. The layers are zstd-supercompressed or uncompressed. Without ASTC support
|
|
40
|
+
* (emulators) frames are decoded to RGBA in software.
|
|
41
|
+
*
|
|
42
|
+
* A TextureView rather than a GLSurfaceView, so it stacks and blends with the views around it.
|
|
43
|
+
* Call its setters and playback methods on the main thread; {@link PlayerCallback} methods are
|
|
44
|
+
* called there too.
|
|
45
|
+
*/
|
|
46
|
+
public class AlphaVideoGpuView extends TextureView implements TextureView.SurfaceTextureListener {
|
|
47
|
+
private static final String TAG = "AlphaVideoGpuView";
|
|
48
|
+
|
|
49
|
+
/** Player events. Every method has an empty default, so implement only what you need. */
|
|
50
|
+
public interface PlayerCallback {
|
|
51
|
+
/** The animation is loaded: frame size in pixels, frame count and frames per second. */
|
|
52
|
+
default void onLoad(int width, int height, int frameCount, double frameRate) {}
|
|
53
|
+
/** The shown frame changed, after seekToFrame, and every frame when frame events are on. */
|
|
54
|
+
default void onFrameUpdate(int frame, int totalFrames) {}
|
|
55
|
+
/** "loading", "ready", "playing", "paused" or "stopped". */
|
|
56
|
+
default void onStateChange(String state) {}
|
|
57
|
+
/** Playback reached the last frame with looping off. */
|
|
58
|
+
default void onFinished() {}
|
|
59
|
+
default void onError(String error) {}
|
|
60
|
+
/** An error with its code, such as {@link #ERROR_NO_SOURCE_MODULE}, or null. Calls {@link #onError(String)} unless overridden. */
|
|
61
|
+
default void onError(String error, @Nullable String code) {
|
|
62
|
+
onError(error);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* {@link PlayerCallback#onError(String, String)}'s code for a data source
|
|
68
|
+
* ({@link #setDataSource}) without the range-source add-on, which reads them.
|
|
69
|
+
*/
|
|
70
|
+
public static final String ERROR_NO_SOURCE_MODULE = "NO_SOURCE_MODULE";
|
|
71
|
+
// A URL played without the add-on is warned of once per process
|
|
72
|
+
private static final AtomicBoolean warnedWithoutAddOn = new AtomicBoolean(false);
|
|
73
|
+
|
|
74
|
+
// GL constants - ASTC formats for all 14 block sizes
|
|
75
|
+
private static final int GL_COMPRESSED_RGBA_ASTC_4x4_KHR = 0x93B0;
|
|
76
|
+
private static final int GL_COMPRESSED_RGBA_ASTC_5x4_KHR = 0x93B1;
|
|
77
|
+
private static final int GL_COMPRESSED_RGBA_ASTC_5x5_KHR = 0x93B2;
|
|
78
|
+
private static final int GL_COMPRESSED_RGBA_ASTC_6x5_KHR = 0x93B3;
|
|
79
|
+
private static final int GL_COMPRESSED_RGBA_ASTC_6x6_KHR = 0x93B4;
|
|
80
|
+
private static final int GL_COMPRESSED_RGBA_ASTC_8x5_KHR = 0x93B5;
|
|
81
|
+
private static final int GL_COMPRESSED_RGBA_ASTC_8x6_KHR = 0x93B6;
|
|
82
|
+
private static final int GL_COMPRESSED_RGBA_ASTC_8x8_KHR = 0x93B7;
|
|
83
|
+
private static final int GL_COMPRESSED_RGBA_ASTC_10x5_KHR = 0x93B8;
|
|
84
|
+
private static final int GL_COMPRESSED_RGBA_ASTC_10x6_KHR = 0x93B9;
|
|
85
|
+
private static final int GL_COMPRESSED_RGBA_ASTC_10x8_KHR = 0x93BA;
|
|
86
|
+
private static final int GL_COMPRESSED_RGBA_ASTC_10x10_KHR = 0x93BB;
|
|
87
|
+
private static final int GL_COMPRESSED_RGBA_ASTC_12x10_KHR = 0x93BC;
|
|
88
|
+
private static final int GL_COMPRESSED_RGBA_ASTC_12x12_KHR = 0x93BD;
|
|
89
|
+
|
|
90
|
+
// How long the zstd stream decoder catches up to a seek before the render loop runs again
|
|
91
|
+
private static final long SKIP_SLICE_NANOS = 16_000_000L;
|
|
92
|
+
|
|
93
|
+
private PlayerCallback playerCallback;
|
|
94
|
+
|
|
95
|
+
// Where what a reader fetches is kept: cacheDir/alpha-video-gpu on disk, or the memory store's name
|
|
96
|
+
private static final String STORE_NAME = "alpha-video-gpu";
|
|
97
|
+
public static final long DEFAULT_MAX_STORAGE_BYTES = 512L * 1024 * 1024;
|
|
98
|
+
public static final String STORAGE_AUTO = "auto";
|
|
99
|
+
public static final String STORAGE_DISK = "disk";
|
|
100
|
+
public static final String STORAGE_MEMORY = "memory";
|
|
101
|
+
public static final String STORAGE_NONE = "none";
|
|
102
|
+
|
|
103
|
+
// Source
|
|
104
|
+
private String mSource;
|
|
105
|
+
private String mCacheKey;
|
|
106
|
+
private Map<String, String> mHeaders = Collections.emptyMap();
|
|
107
|
+
private String mDataSource;
|
|
108
|
+
private String mStorage = STORAGE_AUTO;
|
|
109
|
+
private long mMaxStorageBytes = DEFAULT_MAX_STORAGE_BYTES;
|
|
110
|
+
// The cache key the currently running playback was started for. Compared against
|
|
111
|
+
// mCacheKey to tell a re-signed URL for the same object apart from a new asset.
|
|
112
|
+
private String mPlayingCacheKey;
|
|
113
|
+
// What the running playback reads with, taken when it starts
|
|
114
|
+
private Map<String, String> mPlayingHeaders = Collections.emptyMap();
|
|
115
|
+
private String mPlayingDataSource;
|
|
116
|
+
private volatile boolean mSourceLoaded = false;
|
|
117
|
+
|
|
118
|
+
// Playback settings
|
|
119
|
+
private boolean autoPlay = true;
|
|
120
|
+
private boolean loop = true;
|
|
121
|
+
private float playbackSpeed = 1.0f;
|
|
122
|
+
private boolean enableFrameEvents = false; // Disabled by default for performance
|
|
123
|
+
private final AtomicBoolean isPaused = new AtomicBoolean(false); // setPaused, kept apart from pause()
|
|
124
|
+
|
|
125
|
+
// Animation state
|
|
126
|
+
private int currentFrame = 0;
|
|
127
|
+
private volatile int totalFrames = 0;
|
|
128
|
+
// The frame the render loop shows next, set from the main thread; -1 when there is none
|
|
129
|
+
private final AtomicInteger seekRequest = new AtomicInteger(-1);
|
|
130
|
+
// A clip that doesn't loop has reached its last frame; playing again starts it over
|
|
131
|
+
private volatile boolean ended = false;
|
|
132
|
+
private double frameRate = Ktx2Metadata.DEFAULT_FRAME_RATE;
|
|
133
|
+
|
|
134
|
+
private int frameWidth = 0;
|
|
135
|
+
private int frameHeight = 0;
|
|
136
|
+
|
|
137
|
+
private volatile boolean verbose = false;
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
// OpenGL resources
|
|
141
|
+
private EGLDisplay eglDisplay;
|
|
142
|
+
private EGLContext eglContext;
|
|
143
|
+
private EGLSurface eglSurface;
|
|
144
|
+
|
|
145
|
+
private FloatBuffer vertexBuffer;
|
|
146
|
+
private FloatBuffer texCoordBuffer;
|
|
147
|
+
|
|
148
|
+
// The GPU samples ASTC; without it (emulators) frames are decoded to RGBA in software
|
|
149
|
+
private boolean astcSupported = false;
|
|
150
|
+
|
|
151
|
+
// ASTC block dimensions and GL format (derived from vkFormat, default 4x4)
|
|
152
|
+
private int astcBlockWidth = 4;
|
|
153
|
+
private int astcBlockHeight = 4;
|
|
154
|
+
private int astcGlFormat = GL_COMPRESSED_RGBA_ASTC_4x4_KHR;
|
|
155
|
+
|
|
156
|
+
private int textureArrayId = 0;
|
|
157
|
+
private int textureArrayLayerCount = 0;
|
|
158
|
+
private boolean streamingRingBufferMode = false; // True when using ring buffer for streaming
|
|
159
|
+
private int streamingRingSize = 0; // Size of ring buffer (for layer index calculation)
|
|
160
|
+
private boolean pbosInitialized = false; // True when PBOs are initialized for async uploads
|
|
161
|
+
|
|
162
|
+
// RAM-heavy mode storage (for memoryStrategy: 'ram' in non-stream mode)
|
|
163
|
+
// Uses contiguous direct ByteBuffer for zero-copy GPU uploads
|
|
164
|
+
private ByteBuffer ramFrameBuffer = null; // All decoded frames in single contiguous buffer
|
|
165
|
+
private int ramBytesPerFrame = 0; // Size of each frame in bytes
|
|
166
|
+
private int ramFrameCount = 0; // Total number of frames
|
|
167
|
+
private boolean ramHeavyMode = false; // True when using RAM-heavy non-stream mode
|
|
168
|
+
private int ramUploadedFrame = -1; // Currently uploaded frame in GPU ring buffer
|
|
169
|
+
|
|
170
|
+
// Frames decoded ahead of the shown one while streaming (set via props). Streaming keeps
|
|
171
|
+
// only these, so it also sizes the GPU ring (vram) or the CPU cache (ram).
|
|
172
|
+
private int configStreamingPrefetchAhead = 8;
|
|
173
|
+
// The ram strategy's GPU ring: the previous frame, the shown one and the next two
|
|
174
|
+
private static final int RAM_RING_SIZE = 4;
|
|
175
|
+
|
|
176
|
+
// Memory strategy: "vram", "ram"
|
|
177
|
+
// - vram (default): frames in GPU VRAM (best for many simultaneous players)
|
|
178
|
+
// - ram: Decoded frames in RAM, upload per-frame (lower VRAM usage)
|
|
179
|
+
private String memoryStrategy = "vram";
|
|
180
|
+
|
|
181
|
+
// Stream prop - when true, use streaming decompression for texture arrays
|
|
182
|
+
private boolean streamProp = true;
|
|
183
|
+
|
|
184
|
+
// Threading
|
|
185
|
+
private final Handler mainHandler = new Handler(Looper.getMainLooper());
|
|
186
|
+
private ExecutorService executor;
|
|
187
|
+
private final AtomicBoolean isPlaying = new AtomicBoolean(false);
|
|
188
|
+
private final AtomicBoolean shouldStop = new AtomicBoolean(false);
|
|
189
|
+
private final AtomicBoolean isDestroyed = new AtomicBoolean(false);
|
|
190
|
+
private final AtomicBoolean isPlaybackRunning = new AtomicBoolean(false); // Guards against concurrent startPlayback calls
|
|
191
|
+
// Set when startPlayback() is called while a previous worker is still unwinding.
|
|
192
|
+
// stopPlayback() only raises a flag, so the worker exits asynchronously; without
|
|
193
|
+
// recording the owed restart the view would stay stopped until the surface was
|
|
194
|
+
// destroyed and recreated (i.e. until the user navigated away and back).
|
|
195
|
+
private final AtomicBoolean pendingRestart = new AtomicBoolean(false);
|
|
196
|
+
|
|
197
|
+
// Shaders
|
|
198
|
+
private static final String TEXTURE_ARRAY_FRAGMENT_SHADER =
|
|
199
|
+
"#version 300 es\n" +
|
|
200
|
+
"#ifdef GL_FRAGMENT_PRECISION_HIGH\n" +
|
|
201
|
+
"precision highp float;\n" +
|
|
202
|
+
"precision highp sampler2DArray;\n" +
|
|
203
|
+
"#else\n" +
|
|
204
|
+
"precision mediump float;\n" +
|
|
205
|
+
"precision mediump sampler2DArray;\n" +
|
|
206
|
+
"#endif\n" +
|
|
207
|
+
"in vec2 vTexCoord;\n" +
|
|
208
|
+
"out vec4 fragColor;\n" +
|
|
209
|
+
"uniform sampler2DArray uTextureArray;\n" +
|
|
210
|
+
"uniform float uLayer;\n" +
|
|
211
|
+
"void main() {\n" +
|
|
212
|
+
" fragColor = texture(uTextureArray, vec3(vTexCoord, uLayer));\n" +
|
|
213
|
+
"}\n";
|
|
214
|
+
|
|
215
|
+
private static final String TEXTURE_ARRAY_VERTEX_SHADER =
|
|
216
|
+
"#version 300 es\n" +
|
|
217
|
+
"in vec4 aPosition;\n" +
|
|
218
|
+
"in vec2 aTexCoord;\n" +
|
|
219
|
+
"out vec2 vTexCoord;\n" +
|
|
220
|
+
"void main() {\n" +
|
|
221
|
+
" gl_Position = aPosition;\n" +
|
|
222
|
+
" vTexCoord = aTexCoord;\n" +
|
|
223
|
+
"}\n";
|
|
224
|
+
|
|
225
|
+
// Texture array shader program and uniforms
|
|
226
|
+
private int arrayProgram = 0;
|
|
227
|
+
private int arrayUniformLayerLocation = -1;
|
|
228
|
+
|
|
229
|
+
private static final float[] VERTEX_DATA = {
|
|
230
|
+
-1.0f, -1.0f,
|
|
231
|
+
1.0f, -1.0f,
|
|
232
|
+
-1.0f, 1.0f,
|
|
233
|
+
1.0f, 1.0f
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
private static final float[] TEXCOORD_DATA = {
|
|
237
|
+
0.0f, 1.0f,
|
|
238
|
+
1.0f, 1.0f,
|
|
239
|
+
0.0f, 0.0f,
|
|
240
|
+
1.0f, 0.0f
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
public AlphaVideoGpuView(Context context) {
|
|
244
|
+
super(context);
|
|
245
|
+
init(context);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
public AlphaVideoGpuView(Context context, @Nullable AttributeSet attrs) {
|
|
249
|
+
super(context, attrs);
|
|
250
|
+
init(context);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
public AlphaVideoGpuView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
|
254
|
+
super(context, attrs, defStyleAttr);
|
|
255
|
+
init(context);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private void init(Context context) {
|
|
259
|
+
setSurfaceTextureListener(this);
|
|
260
|
+
setOpaque(false); // Critical for transparency
|
|
261
|
+
isDestroyed.set(false);
|
|
262
|
+
|
|
263
|
+
// Don't intercept touches
|
|
264
|
+
setClickable(false);
|
|
265
|
+
setFocusable(false);
|
|
266
|
+
setFocusableInTouchMode(false);
|
|
267
|
+
|
|
268
|
+
// Setup vertex buffers
|
|
269
|
+
vertexBuffer = ByteBuffer.allocateDirect(VERTEX_DATA.length * 4)
|
|
270
|
+
.order(ByteOrder.nativeOrder())
|
|
271
|
+
.asFloatBuffer()
|
|
272
|
+
.put(VERTEX_DATA);
|
|
273
|
+
vertexBuffer.position(0);
|
|
274
|
+
|
|
275
|
+
texCoordBuffer = ByteBuffer.allocateDirect(TEXCOORD_DATA.length * 4)
|
|
276
|
+
.order(ByteOrder.nativeOrder())
|
|
277
|
+
.asFloatBuffer()
|
|
278
|
+
.put(TEXCOORD_DATA);
|
|
279
|
+
texCoordBuffer.position(0);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
private synchronized ExecutorService getOrCreateExecutor() {
|
|
283
|
+
if (executor == null || executor.isShutdown()) {
|
|
284
|
+
// Use a thread pool with 2 threads to allow preloading while playback runs
|
|
285
|
+
executor = Executors.newFixedThreadPool(2);
|
|
286
|
+
}
|
|
287
|
+
return executor;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
private synchronized void shutdownExecutor() {
|
|
291
|
+
if (executor != null && !executor.isShutdown()) {
|
|
292
|
+
executor.shutdownNow();
|
|
293
|
+
executor = null;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
private void notifyStateChange(String state) {
|
|
298
|
+
if (playerCallback != null) playerCallback.onStateChange(state);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
private void notifyLoad() {
|
|
302
|
+
if (playerCallback != null) playerCallback.onLoad(frameWidth, frameHeight, totalFrames, frameRate);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
private void notifyFrameUpdate() {
|
|
306
|
+
if (playerCallback != null) playerCallback.onFrameUpdate(currentFrame, totalFrames);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
private void notifyFinished() {
|
|
310
|
+
if (playerCallback != null) playerCallback.onFinished();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
private void notifyError(String error) {
|
|
314
|
+
notifyError(error, null);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
private void notifyError(String error, @Nullable String code) {
|
|
318
|
+
Log.e(TAG, "Error: " + error);
|
|
319
|
+
if (playerCallback != null) playerCallback.onError(error, code);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Public API
|
|
323
|
+
|
|
324
|
+
/** Receives this view's events on the main thread; null to stop. */
|
|
325
|
+
public void setPlayerCallback(@Nullable PlayerCallback callback) {
|
|
326
|
+
playerCallback = callback;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* What to play, or null to stop: https:// or http:// (read by range as it plays, and kept as
|
|
331
|
+
* {@link #setStorage} says), file:// or an absolute path, asset:// (APK assets), content://,
|
|
332
|
+
* a res/raw resource name, or with {@link #setDataSource} any URI that data source reads.
|
|
333
|
+
* Set the other source properties first. Playback starts once the view's surface exists.
|
|
334
|
+
*/
|
|
335
|
+
public void setSource(String source) {
|
|
336
|
+
// Callers (React re-renders, for one) set the same source again, and for an unchanged source the
|
|
337
|
+
// stop-then-start below is pure churn: it discards the decoded ring buffer
|
|
338
|
+
// and briefly leaves the view with no running worker. Only act when the value
|
|
339
|
+
// actually changed, or when nothing is currently playing (so a stalled or
|
|
340
|
+
// finished player can still be revived by setting the same source again).
|
|
341
|
+
boolean playing = isPlaybackRunning.get() && !shouldStop.get();
|
|
342
|
+
boolean sameReads = mHeaders.equals(mPlayingHeaders) && Objects.equals(mDataSource, mPlayingDataSource);
|
|
343
|
+
if (source != null && source.equals(mSource) && playing && sameReads) {
|
|
344
|
+
logD("setSource: unchanged and still playing, ignoring");
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// A presigned URL is re-signed every time the app lists its media, so the URL
|
|
349
|
+
// changes while still addressing the same object. The cache key is the stable
|
|
350
|
+
// identity, so when it matches what is already on screen this is a credential
|
|
351
|
+
// refresh rather than a new asset, and restarting would visibly interrupt a
|
|
352
|
+
// loop that is supposed to run until unmount.
|
|
353
|
+
if (source != null && playing && sameReads
|
|
354
|
+
&& mCacheKey != null && !mCacheKey.isEmpty()
|
|
355
|
+
&& mCacheKey.equals(mPlayingCacheKey)) {
|
|
356
|
+
logD("setSource: same cacheKey (" + mCacheKey + ") still playing, ignoring URL change");
|
|
357
|
+
mSource = source;
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
logD("setSource: " + (source != null ? source.substring(0, Math.min(80, source.length())) : "null"));
|
|
362
|
+
|
|
363
|
+
stopPlayback();
|
|
364
|
+
mSource = source;
|
|
365
|
+
mSourceLoaded = false;
|
|
366
|
+
|
|
367
|
+
if (source != null && isAvailable()) {
|
|
368
|
+
startPlayback();
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Names what the store keeps a URL's bytes under, instead of the URL, for URLs that change
|
|
374
|
+
* while the file doesn't (presigned URLs). A source with the same cache key as the one playing
|
|
375
|
+
* keeps playing. Set it before the source.
|
|
376
|
+
*/
|
|
377
|
+
public void setCacheKey(String cacheKey) {
|
|
378
|
+
mCacheKey = cacheKey;
|
|
379
|
+
logD("setCacheKey: " + cacheKey);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** Sent with every request for an http(s) source, and given to a data source; null for none. */
|
|
383
|
+
public void setHeaders(@Nullable Map<String, String> headers) {
|
|
384
|
+
mHeaders = headers == null || headers.isEmpty()
|
|
385
|
+
? Collections.emptyMap() : Collections.unmodifiableMap(new LinkedHashMap<>(headers));
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Reads the source through the data source registered under this name with the range-source
|
|
390
|
+
* add-on ({@code dev.mediacache.rangesource.RangeDataSources.register}), whatever its scheme;
|
|
391
|
+
* null for the built-in ones.
|
|
392
|
+
*/
|
|
393
|
+
public void setDataSource(@Nullable String name) {
|
|
394
|
+
mDataSource = name == null || name.isEmpty() ? null : name;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Where a URL's or data source's bytes are kept between plays: {@code "disk"} (in the cache
|
|
399
|
+
* folder), {@code "memory"} (for the process's lifetime) or {@code "none"}. {@code "auto"}, the
|
|
400
|
+
* default, is disk for URLs and none for a data source, whose caching is its own. The store is
|
|
401
|
+
* partial: it keeps what has been played, and a clip held whole plays again offline.
|
|
402
|
+
*/
|
|
403
|
+
public void setStorage(@Nullable String storage) {
|
|
404
|
+
switch (storage == null ? STORAGE_AUTO : storage) {
|
|
405
|
+
case STORAGE_DISK:
|
|
406
|
+
case STORAGE_MEMORY:
|
|
407
|
+
case STORAGE_NONE:
|
|
408
|
+
mStorage = storage;
|
|
409
|
+
break;
|
|
410
|
+
default:
|
|
411
|
+
mStorage = STORAGE_AUTO;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* The cap of this player's store, over every clip in it, least recently played out first;
|
|
417
|
+
* 0 or less for the default, 512 MB. Every view shares the store, under the cap set last.
|
|
418
|
+
*/
|
|
419
|
+
public void setMaxStorageBytes(long maxStorageBytes) {
|
|
420
|
+
mMaxStorageBytes = maxStorageBytes > 0 ? maxStorageBytes : DEFAULT_MAX_STORAGE_BYTES;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
public void setAutoPlay(boolean autoPlay) {
|
|
424
|
+
this.autoPlay = autoPlay;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
public void setLoop(boolean loop) {
|
|
428
|
+
this.loop = loop;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
public void setPlaybackSpeed(float speed) {
|
|
432
|
+
this.playbackSpeed = speed;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
public void setEnableFrameEvents(boolean enable) {
|
|
436
|
+
this.enableFrameEvents = enable;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
public void setStreamingPrefetchAhead(int frames) {
|
|
440
|
+
this.configStreamingPrefetchAhead = Math.max(1, Math.min(100, frames));
|
|
441
|
+
logD("setStreamingPrefetchAhead: " + this.configStreamingPrefetchAhead);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Set memory strategy for frame storage.
|
|
446
|
+
* - "vram" (default): frames in GPU VRAM (best for many simultaneous players)
|
|
447
|
+
* - "ram": Decoded frames in RAM, upload per-frame (lower VRAM usage)
|
|
448
|
+
* Any other value means "vram".
|
|
449
|
+
*/
|
|
450
|
+
public void setMemoryStrategy(String strategy) {
|
|
451
|
+
this.memoryStrategy = "ram".equals(strategy) ? "ram" : "vram";
|
|
452
|
+
logD("setMemoryStrategy: " + this.memoryStrategy);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Set streaming mode for texture array playback.
|
|
457
|
+
* When true (default): Use streaming decompression with ring buffer (constant memory usage).
|
|
458
|
+
* When false: Load all frames at once.
|
|
459
|
+
*/
|
|
460
|
+
public void setStream(boolean stream) {
|
|
461
|
+
this.streamProp = stream;
|
|
462
|
+
logD("setStream: " + stream);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Pauses or resumes playback, keeping the loaded frames (for screen focus and blur).
|
|
467
|
+
* Resuming plays again only when autoPlay is on.
|
|
468
|
+
*/
|
|
469
|
+
public void setPaused(boolean paused) {
|
|
470
|
+
boolean wasPaused = isPaused.getAndSet(paused);
|
|
471
|
+
if (paused != wasPaused) {
|
|
472
|
+
logD("setPaused: " + paused);
|
|
473
|
+
if (paused) {
|
|
474
|
+
// Stop playback
|
|
475
|
+
isPlaying.set(false);
|
|
476
|
+
} else {
|
|
477
|
+
// Resume playback if we were playing before and source is loaded
|
|
478
|
+
if (mSourceLoaded && autoPlay) {
|
|
479
|
+
isPlaying.set(true);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/** Logs debug and info traces when true; warnings and errors are always logged. */
|
|
486
|
+
public void setVerbose(boolean verbose) {
|
|
487
|
+
this.verbose = verbose;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/** Plays from the shown frame, or from the start once a clip that doesn't loop has ended. */
|
|
491
|
+
public void play() {
|
|
492
|
+
if (mSourceLoaded && !isPlaying.getAndSet(true)) {
|
|
493
|
+
notifyStateChange("playing");
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
public void pause() {
|
|
498
|
+
if (isPlaying.getAndSet(false)) {
|
|
499
|
+
notifyStateChange("paused");
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/** Pauses on the first frame. */
|
|
504
|
+
public void stop() {
|
|
505
|
+
isPlaying.set(false);
|
|
506
|
+
seekToFrame(0);
|
|
507
|
+
notifyStateChange("stopped");
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Shows that frame (clamped to the clip) and keeps playing or stays paused. Ignored until
|
|
512
|
+
* onLoad. In stream mode the frames before it are decoded again, unless they are buffered.
|
|
513
|
+
*/
|
|
514
|
+
public void seekToFrame(int frame) {
|
|
515
|
+
int frames = totalFrames;
|
|
516
|
+
if (!mSourceLoaded || frames <= 0) return;
|
|
517
|
+
int target = Math.max(0, Math.min(frame, frames - 1));
|
|
518
|
+
seekRequest.set(target);
|
|
519
|
+
if (playerCallback != null) playerCallback.onFrameUpdate(target, frames);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/** Seeks to the frame showing at that time, in seconds. */
|
|
523
|
+
public void seekToTime(double seconds) {
|
|
524
|
+
double frame = Math.floor(Math.max(seconds, 0) * frameRate + 1e-6);
|
|
525
|
+
seekToFrame((int) Math.min(frame, Integer.MAX_VALUE));
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/** Stops playback and its threads. Detaching the view from its window does the same. */
|
|
529
|
+
public void destroy() {
|
|
530
|
+
isDestroyed.set(true);
|
|
531
|
+
shouldStop.set(true);
|
|
532
|
+
isPlaying.set(false);
|
|
533
|
+
shutdownExecutor();
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
private void startPlayback() {
|
|
537
|
+
if (mSource == null || !isAvailable() || isDestroyed.get()) {
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Prevent concurrent playback attempts. A previous worker may still be
|
|
542
|
+
// unwinding after stopPlayback() raised shouldStop; in that case the restart
|
|
543
|
+
// is owed rather than redundant, so record it and let the exiting worker
|
|
544
|
+
// relaunch. Returning without doing this leaves shouldStop raised with no
|
|
545
|
+
// worker running, which strands the view until the surface is recreated.
|
|
546
|
+
if (!isPlaybackRunning.compareAndSet(false, true)) {
|
|
547
|
+
logD("startPlayback: worker still running, queueing restart");
|
|
548
|
+
pendingRestart.set(true);
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
shouldStop.set(false);
|
|
553
|
+
mPlayingCacheKey = mCacheKey;
|
|
554
|
+
mPlayingHeaders = mHeaders;
|
|
555
|
+
mPlayingDataSource = mDataSource;
|
|
556
|
+
|
|
557
|
+
try {
|
|
558
|
+
getOrCreateExecutor().execute(() -> {
|
|
559
|
+
try {
|
|
560
|
+
playAnimation();
|
|
561
|
+
} catch (Exception e) {
|
|
562
|
+
Log.e(TAG, "Playback error", e);
|
|
563
|
+
mainHandler.post(() -> notifyError(e.getMessage()));
|
|
564
|
+
} finally {
|
|
565
|
+
cleanupGL();
|
|
566
|
+
mPlayingCacheKey = null;
|
|
567
|
+
isPlaybackRunning.set(false);
|
|
568
|
+
// Clear the guard first, then service any restart requested while
|
|
569
|
+
// this worker was shutting down. Hopping to the main thread keeps
|
|
570
|
+
// the view/surface reads in startPlayback() on the UI thread.
|
|
571
|
+
if (pendingRestart.getAndSet(false) && !isDestroyed.get()) {
|
|
572
|
+
mainHandler.post(this::startPlayback);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
} catch (Exception e) {
|
|
577
|
+
Log.e(TAG, "Failed to start playback", e);
|
|
578
|
+
isPlaybackRunning.set(false);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
private void stopPlayback() {
|
|
583
|
+
shouldStop.set(true);
|
|
584
|
+
isPlaying.set(false);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
private void playAnimation() {
|
|
588
|
+
final String uri = mSource;
|
|
589
|
+
final AlphaVideoGpuSource local;
|
|
590
|
+
try {
|
|
591
|
+
local = resolveSource(uri, mPlayingCacheKey);
|
|
592
|
+
} catch (NoSourceModuleException e) {
|
|
593
|
+
mainHandler.post(() -> notifyError(e.getMessage(), ERROR_NO_SOURCE_MODULE));
|
|
594
|
+
return;
|
|
595
|
+
} catch (IOException | IllegalArgumentException e) {
|
|
596
|
+
if (shouldStop.get()) return; // stopped while opening
|
|
597
|
+
mainHandler.post(() -> notifyError("Loading " + uri + " failed: " + e.getMessage()));
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
try {
|
|
601
|
+
if (!shouldStop.get()) playSource(local);
|
|
602
|
+
} finally {
|
|
603
|
+
AlphaVideoGpuReader reader = local.reader();
|
|
604
|
+
if (reader != null) reader.close();
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
private void playSource(AlphaVideoGpuSource local) {
|
|
609
|
+
// Initialize OpenGL
|
|
610
|
+
SurfaceTexture surfaceTexture = getSurfaceTexture();
|
|
611
|
+
if (surfaceTexture == null || !initGL(surfaceTexture)) {
|
|
612
|
+
mainHandler.post(() -> notifyError("Failed to initialize OpenGL"));
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
astcSupported = detectASTCSupport();
|
|
617
|
+
|
|
618
|
+
// The header, level index and metadata only; streaming reads the level as it plays
|
|
619
|
+
byte[] header;
|
|
620
|
+
try {
|
|
621
|
+
header = readHeader(local);
|
|
622
|
+
} catch (IOException e) {
|
|
623
|
+
if (shouldStop.get()) return;
|
|
624
|
+
mainHandler.post(() -> notifyError("Failed to read " + local + ": " + e.getMessage()));
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
String metadataError = applyMetadata(Ktx2Metadata.read(header));
|
|
629
|
+
if (metadataError != null) {
|
|
630
|
+
mainHandler.post(() -> notifyError(metadataError));
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
if (streamProp) {
|
|
635
|
+
// Check supercompressionScheme from KTX2 header
|
|
636
|
+
int supercompressionScheme = getSupercompressionScheme(header);
|
|
637
|
+
logI("Streaming mode enabled (stream=true), supercompression=" + supercompressionScheme);
|
|
638
|
+
|
|
639
|
+
if (supercompressionScheme == 2) {
|
|
640
|
+
playInternalZstdStreamingAnimation(local, header);
|
|
641
|
+
return;
|
|
642
|
+
} else if (supercompressionScheme == 0) {
|
|
643
|
+
// Uncompressed - use mmap streaming
|
|
644
|
+
playUncompressedStreamingAnimation(local);
|
|
645
|
+
return;
|
|
646
|
+
} else {
|
|
647
|
+
Log.w(TAG, "Unsupported supercompression for streaming: " + supercompressionScheme + ", falling back to non-stream");
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// Load textures (non-streaming path)
|
|
652
|
+
try {
|
|
653
|
+
loadTextureArray(local);
|
|
654
|
+
} catch (Exception e) {
|
|
655
|
+
if (shouldStop.get()) return;
|
|
656
|
+
mainHandler.post(() -> notifyError("Failed to load textures: " + e.getMessage()));
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Initialize shaders
|
|
661
|
+
if (!initShaders()) {
|
|
662
|
+
mainHandler.post(() -> notifyError("Failed to initialize shaders"));
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
announceLoaded();
|
|
667
|
+
|
|
668
|
+
logD("Starting render loop");
|
|
669
|
+
|
|
670
|
+
FrameClock clock = new FrameClock(frameIntervalNanos());
|
|
671
|
+
currentFrame = 0;
|
|
672
|
+
boolean frameOwed = true;
|
|
673
|
+
boolean wasPlaying = false;
|
|
674
|
+
try {
|
|
675
|
+
while (!shouldStop.get() && !isDestroyed.get()) {
|
|
676
|
+
int seek = takeSeek();
|
|
677
|
+
if (seek >= 0) {
|
|
678
|
+
currentFrame = seek;
|
|
679
|
+
frameOwed = true;
|
|
680
|
+
wasPlaying = false;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
if (!isPlaying.get()) {
|
|
684
|
+
if (frameOwed) {
|
|
685
|
+
renderFrame();
|
|
686
|
+
frameOwed = false;
|
|
687
|
+
}
|
|
688
|
+
wasPlaying = false;
|
|
689
|
+
Thread.sleep(16);
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
if (!wasPlaying) {
|
|
693
|
+
clock.restart();
|
|
694
|
+
wasPlaying = true;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
renderFrame();
|
|
698
|
+
frameOwed = false;
|
|
699
|
+
if (enableFrameEvents) {
|
|
700
|
+
mainHandler.post(() -> notifyFrameUpdate());
|
|
701
|
+
}
|
|
702
|
+
clock.awaitNext();
|
|
703
|
+
|
|
704
|
+
if (currentFrame + 1 < totalFrames) {
|
|
705
|
+
currentFrame++;
|
|
706
|
+
} else if (loop) {
|
|
707
|
+
currentFrame = 0;
|
|
708
|
+
} else {
|
|
709
|
+
finish();
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
} catch (InterruptedException e) {
|
|
713
|
+
Thread.currentThread().interrupt();
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/** Reports the load, and starts playing when autoPlay is on and the view isn't paused. */
|
|
718
|
+
private void announceLoaded() {
|
|
719
|
+
ended = false;
|
|
720
|
+
seekRequest.set(-1);
|
|
721
|
+
mSourceLoaded = true;
|
|
722
|
+
mainHandler.post(() -> {
|
|
723
|
+
notifyLoad();
|
|
724
|
+
notifyStateChange("ready");
|
|
725
|
+
});
|
|
726
|
+
if (autoPlay && !isPaused.get()) {
|
|
727
|
+
isPlaying.set(true);
|
|
728
|
+
mainHandler.post(() -> notifyStateChange("playing"));
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
private long frameIntervalNanos() {
|
|
733
|
+
return (long) (1_000_000_000.0 / (frameRate * playbackSpeed));
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* The frame a render loop should show next, or -1 to carry on: the requested seek, or
|
|
738
|
+
* frame 0 when a clip that ended is played again.
|
|
739
|
+
*/
|
|
740
|
+
private int takeSeek() {
|
|
741
|
+
int seek = seekRequest.getAndSet(-1);
|
|
742
|
+
if (seek < 0 && ended && isPlaying.get()) seek = 0;
|
|
743
|
+
if (seek >= 0) ended = false;
|
|
744
|
+
return seek;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/** Holds the last frame of a clip that doesn't loop. */
|
|
748
|
+
private void finish() {
|
|
749
|
+
ended = true;
|
|
750
|
+
isPlaying.set(false);
|
|
751
|
+
mainHandler.post(() -> {
|
|
752
|
+
notifyFinished();
|
|
753
|
+
notifyStateChange("stopped");
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/** Paces frames from the moment playback starts, resumes or seeks. */
|
|
758
|
+
private static final class FrameClock {
|
|
759
|
+
private final long intervalNs;
|
|
760
|
+
private long dueNs;
|
|
761
|
+
|
|
762
|
+
FrameClock(long intervalNs) {
|
|
763
|
+
this.intervalNs = intervalNs;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/** The current frame is due now. */
|
|
767
|
+
void restart() {
|
|
768
|
+
dueNs = System.nanoTime();
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/** Sleeps until the next frame is due. */
|
|
772
|
+
void awaitNext() throws InterruptedException {
|
|
773
|
+
dueNs += intervalNs;
|
|
774
|
+
long sleepNs = dueNs - System.nanoTime();
|
|
775
|
+
if (sleepNs > 1_000_000) {
|
|
776
|
+
Thread.sleep(sleepNs / 1_000_000, (int) (sleepNs % 1_000_000));
|
|
777
|
+
} else if (sleepNs < -intervalNs) {
|
|
778
|
+
// Too far behind to catch up (a slow decode), so pace from now
|
|
779
|
+
dueNs = System.nanoTime();
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* Drops the next {@code bytes} of decompressed data: first what the accumulator holds, then
|
|
786
|
+
* the rest through {@code bytesToSkip} as the stream is read.
|
|
787
|
+
*/
|
|
788
|
+
private static void skipDecodedBytes(ByteBuffer accumulator, long[] bytesToSkip, long bytes) {
|
|
789
|
+
int held = accumulator.position();
|
|
790
|
+
if (held > bytes) {
|
|
791
|
+
accumulator.flip();
|
|
792
|
+
accumulator.position((int) bytes);
|
|
793
|
+
accumulator.compact();
|
|
794
|
+
} else {
|
|
795
|
+
accumulator.clear();
|
|
796
|
+
bytesToSkip[0] += bytes - held;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/** Forgets every decoded frame after a seek, returning the CPU buffers to the pool. */
|
|
801
|
+
private static void dropStreamedFrames(java.util.Map<Integer, byte[]> frameCache,
|
|
802
|
+
@Nullable java.util.Queue<byte[]> bufferPool,
|
|
803
|
+
int[] ringSlots, int bytesPerLayer) {
|
|
804
|
+
if (bufferPool != null) {
|
|
805
|
+
for (byte[] buffer : frameCache.values()) {
|
|
806
|
+
if (buffer.length == bytesPerLayer) bufferPool.offer(buffer);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
frameCache.clear();
|
|
810
|
+
Arrays.fill(ringSlots, -1);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/**
|
|
814
|
+
* The file's first bytes, through the end of its key/value data: the header, the level index
|
|
815
|
+
* and the metadata. 16 KB, or more when the key/value data runs past it.
|
|
816
|
+
*/
|
|
817
|
+
private byte[] readHeader(AlphaVideoGpuSource local) throws IOException {
|
|
818
|
+
final int guess = 16 * 1024;
|
|
819
|
+
byte[] head = local.readHead(getContext(), guess);
|
|
820
|
+
if (head.length < 80) throw new IOException("Too short for a KTX2 file (" + head.length + " bytes)");
|
|
821
|
+
ByteBuffer header = ByteBuffer.wrap(head).order(ByteOrder.LITTLE_ENDIAN);
|
|
822
|
+
long kvdEnd = (header.getInt(56) & 0xFFFFFFFFL) + (header.getInt(60) & 0xFFFFFFFFL);
|
|
823
|
+
if (kvdEnd <= head.length || head.length < guess) return head;
|
|
824
|
+
if (kvdEnd > 16L * 1024 * 1024) throw new IOException("Key/value data of " + kvdEnd + " bytes");
|
|
825
|
+
return local.readHead(getContext(), (int) kvdEnd);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* The seek table at the end of the level, read from the level's last bytes; null when the
|
|
830
|
+
* level is one stream.
|
|
831
|
+
*/
|
|
832
|
+
@Nullable
|
|
833
|
+
private ZstdSeekTable readSeekTable(AlphaVideoGpuSource local, long levelOffset, long levelLength,
|
|
834
|
+
long decompressedLength) throws IOException {
|
|
835
|
+
if (levelLength <= 0) return null;
|
|
836
|
+
long levelEnd = levelOffset + levelLength;
|
|
837
|
+
int count = (int) Math.min(levelLength, ZstdSeekTable.TAIL_GUESS);
|
|
838
|
+
byte[] tail = local.readRange(getContext(), levelEnd - count, count);
|
|
839
|
+
long needed = ZstdSeekTable.tailBytesNeeded(tail);
|
|
840
|
+
if (needed > tail.length && needed <= levelLength && needed <= Integer.MAX_VALUE) {
|
|
841
|
+
tail = local.readRange(getContext(), levelEnd - needed, (int) needed);
|
|
842
|
+
}
|
|
843
|
+
return ZstdSeekTable.parseTail(tail, levelLength, decompressedLength);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/**
|
|
847
|
+
* Play uncompressed streaming animation from .ktx2 file.
|
|
848
|
+
* Uses memory-mapped file for true zero-copy frame uploads.
|
|
849
|
+
* This is the fastest streaming mode - no decompression overhead.
|
|
850
|
+
* Does not require pre-loading the file into memory - reads header from mmap.
|
|
851
|
+
*/
|
|
852
|
+
private void playUncompressedStreamingAnimation(AlphaVideoGpuSource local) {
|
|
853
|
+
logI("Starting uncompressed streaming animation: " + local);
|
|
854
|
+
|
|
855
|
+
// Memory-map the file for zero-copy reads - do this FIRST to avoid loading file into RAM.
|
|
856
|
+
// A URL or data source is read whole into memory instead: uncompressed frames are as
|
|
857
|
+
// large as decoded ones, so such files are for local playback, and zstd ones stream.
|
|
858
|
+
final ByteBuffer mmapBuffer;
|
|
859
|
+
try {
|
|
860
|
+
mmapBuffer = local.reader() != null ? local.readAllDirect(getContext()) : local.map(getContext());
|
|
861
|
+
} catch (Exception e) {
|
|
862
|
+
if (shouldStop.get()) return;
|
|
863
|
+
Log.e(TAG, "Failed to read " + local + ": " + e.getMessage());
|
|
864
|
+
mainHandler.post(() -> notifyError("Failed to stream " + local + ": " + e.getMessage()));
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
mmapBuffer.order(ByteOrder.LITTLE_ENDIAN);
|
|
868
|
+
logI("Streaming from " + mmapBuffer.capacity() + " bytes " + (local.reader() != null ? "in memory" : "mapped"));
|
|
869
|
+
|
|
870
|
+
// Parse KTX2 header directly from mmap buffer (no RAM copy needed)
|
|
871
|
+
int vkFormat = mmapBuffer.getInt(12);
|
|
872
|
+
int pixelWidth = mmapBuffer.getInt(20);
|
|
873
|
+
int pixelHeight = mmapBuffer.getInt(24);
|
|
874
|
+
int layerCount = mmapBuffer.getInt(32);
|
|
875
|
+
if (layerCount == 0) layerCount = 1;
|
|
876
|
+
long level0ByteOffset = mmapBuffer.getLong(80);
|
|
877
|
+
|
|
878
|
+
// Configure ASTC format from vkFormat
|
|
879
|
+
configureASTCFromVkFormat(vkFormat);
|
|
880
|
+
|
|
881
|
+
// Calculate bytes per frame using dynamic ASTC block size
|
|
882
|
+
int bytesPerLayer = astcBytesPerLayer(pixelWidth, pixelHeight);
|
|
883
|
+
|
|
884
|
+
logI("Uncompressed streaming: " + pixelWidth + "x" + pixelHeight + ", " +
|
|
885
|
+
layerCount + " layers, " + bytesPerLayer + " bytes/layer (ASTC " + astcBlockWidth + "x" + astcBlockHeight + "), data offset=" + level0ByteOffset);
|
|
886
|
+
|
|
887
|
+
// Frames are read from the mapping as they're shown, nothing ahead, so a small ring does
|
|
888
|
+
// for either memory strategy
|
|
889
|
+
boolean useVramStrategy = !"ram".equals(memoryStrategy);
|
|
890
|
+
boolean useAstc = astcSupported;
|
|
891
|
+
int gpuCacheSize = Math.min(RAM_RING_SIZE, layerCount);
|
|
892
|
+
|
|
893
|
+
logI("Uncompressed streaming params: strategy=" + memoryStrategy +
|
|
894
|
+
", gpuCache=" + gpuCacheSize + ", vramMode=" + useVramStrategy);
|
|
895
|
+
|
|
896
|
+
// Create GPU texture array
|
|
897
|
+
textureArrayId = AlphaVideoGpuNative.nativeCreateTextureArray(pixelWidth, pixelHeight, gpuCacheSize, useAstc, astcGlFormat);
|
|
898
|
+
if (textureArrayId == 0) {
|
|
899
|
+
mainHandler.post(() -> notifyError("Failed to create texture array for uncompressed streaming"));
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
logI("Created uncompressed streaming texture array: ID=" + textureArrayId +
|
|
904
|
+
", " + pixelWidth + "x" + pixelHeight + ", " + gpuCacheSize + " GPU slots");
|
|
905
|
+
|
|
906
|
+
// Initialize ring buffer tracking
|
|
907
|
+
streamingRingBufferMode = true;
|
|
908
|
+
streamingRingSize = gpuCacheSize;
|
|
909
|
+
int[] ringSlots = new int[gpuCacheSize];
|
|
910
|
+
Arrays.fill(ringSlots, -1);
|
|
911
|
+
|
|
912
|
+
// Initialize PBOs for async uploads (only for ASTC compressed format)
|
|
913
|
+
if (useAstc) {
|
|
914
|
+
pbosInitialized = AlphaVideoGpuNative.nativeInitPBOs(textureArrayId, bytesPerLayer);
|
|
915
|
+
if (pbosInitialized) {
|
|
916
|
+
logI("PBOs initialized for uncompressed streaming: " + bytesPerLayer + " bytes per buffer");
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// Initialize shaders
|
|
921
|
+
if (!initShaders()) {
|
|
922
|
+
mainHandler.post(() -> notifyError("Failed to initialize shaders"));
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
totalFrames = layerCount;
|
|
927
|
+
announceLoaded();
|
|
928
|
+
|
|
929
|
+
// Frame timing - use metadata frameRate (already parsed)
|
|
930
|
+
long frameIntervalNs = frameIntervalNanos();
|
|
931
|
+
logI("Uncompressed streaming timing: frameRate=" + frameRate + ", playbackSpeed=" + playbackSpeed +
|
|
932
|
+
", frameIntervalNs=" + frameIntervalNs + " (" + (frameIntervalNs / 1_000_000.0) + "ms)");
|
|
933
|
+
|
|
934
|
+
// Capture finals for lambda
|
|
935
|
+
final int finalWidth = pixelWidth;
|
|
936
|
+
final int finalHeight = pixelHeight;
|
|
937
|
+
final int finalBytesPerLayer = bytesPerLayer;
|
|
938
|
+
final int finalLayerCount = layerCount;
|
|
939
|
+
final int finalGpuCacheSize = gpuCacheSize;
|
|
940
|
+
final long finalDataOffset = level0ByteOffset;
|
|
941
|
+
final boolean finalUseAstc = useAstc;
|
|
942
|
+
|
|
943
|
+
// Main playback loop - simple and fast for uncompressed data. Any frame can be read
|
|
944
|
+
// straight from the mapping, so a seek only moves the playhead.
|
|
945
|
+
int debugFrameCounter = 0;
|
|
946
|
+
FrameClock clock = new FrameClock(frameIntervalNs);
|
|
947
|
+
currentFrame = 0;
|
|
948
|
+
boolean frameOwed = true;
|
|
949
|
+
boolean wasPlaying = false;
|
|
950
|
+
try {
|
|
951
|
+
while (!shouldStop.get() && !isDestroyed.get()) {
|
|
952
|
+
int seek = takeSeek();
|
|
953
|
+
if (seek >= 0) {
|
|
954
|
+
currentFrame = seek;
|
|
955
|
+
frameOwed = true;
|
|
956
|
+
wasPlaying = false;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
boolean playing = isPlaying.get();
|
|
960
|
+
if (!playing && !frameOwed) {
|
|
961
|
+
wasPlaying = false;
|
|
962
|
+
Thread.sleep(16);
|
|
963
|
+
continue;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// Calculate GPU slot for this frame
|
|
967
|
+
int gpuSlot = currentFrame % finalGpuCacheSize;
|
|
968
|
+
|
|
969
|
+
// Check if frame needs to be uploaded
|
|
970
|
+
if (ringSlots[gpuSlot] != currentFrame) {
|
|
971
|
+
// Calculate offset in mmap buffer for this frame
|
|
972
|
+
long frameOffset = finalDataOffset + (long)currentFrame * finalBytesPerLayer;
|
|
973
|
+
|
|
974
|
+
// Upload directly from mmap buffer (true zero-copy!). The limit goes back
|
|
975
|
+
// to the end first, since a frame after the last one read lies past it.
|
|
976
|
+
mmapBuffer.clear();
|
|
977
|
+
mmapBuffer.position((int)frameOffset);
|
|
978
|
+
mmapBuffer.limit((int)frameOffset + finalBytesPerLayer);
|
|
979
|
+
ByteBuffer frameSlice = mmapBuffer.slice();
|
|
980
|
+
frameSlice.order(ByteOrder.nativeOrder());
|
|
981
|
+
|
|
982
|
+
// Use PBO for async upload when available
|
|
983
|
+
boolean uploaded = pbosInitialized ?
|
|
984
|
+
AlphaVideoGpuNative.nativeUploadTextureArrayLayerPBO(
|
|
985
|
+
textureArrayId, gpuSlot, finalWidth, finalHeight,
|
|
986
|
+
frameSlice, finalBytesPerLayer, finalUseAstc,
|
|
987
|
+
astcGlFormat, astcBlockWidth, astcBlockHeight) :
|
|
988
|
+
AlphaVideoGpuNative.nativeUploadTextureArrayLayerFromBuffer(
|
|
989
|
+
textureArrayId, gpuSlot, finalWidth, finalHeight,
|
|
990
|
+
frameSlice, finalBytesPerLayer, finalUseAstc,
|
|
991
|
+
astcGlFormat, astcBlockWidth, astcBlockHeight);
|
|
992
|
+
|
|
993
|
+
if (uploaded) {
|
|
994
|
+
ringSlots[gpuSlot] = currentFrame;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// Render (includes eglSwapBuffers)
|
|
999
|
+
renderTextureArrayFrame();
|
|
1000
|
+
frameOwed = false;
|
|
1001
|
+
if (!playing) {
|
|
1002
|
+
wasPlaying = false;
|
|
1003
|
+
continue;
|
|
1004
|
+
}
|
|
1005
|
+
if (!wasPlaying) {
|
|
1006
|
+
clock.restart();
|
|
1007
|
+
wasPlaying = true;
|
|
1008
|
+
}
|
|
1009
|
+
if (enableFrameEvents) {
|
|
1010
|
+
mainHandler.post(() -> notifyFrameUpdate());
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// Debug logging every 30 frames
|
|
1014
|
+
if (debugFrameCounter % 30 == 0) {
|
|
1015
|
+
logD("Uncompressed frame " + currentFrame + "/" + finalLayerCount +
|
|
1016
|
+
", gpuSlot=" + (currentFrame % finalGpuCacheSize) +
|
|
1017
|
+
", ringSize=" + streamingRingSize);
|
|
1018
|
+
}
|
|
1019
|
+
debugFrameCounter++;
|
|
1020
|
+
|
|
1021
|
+
// Frame timing - wait until next frame time
|
|
1022
|
+
clock.awaitNext();
|
|
1023
|
+
|
|
1024
|
+
if (currentFrame + 1 < finalLayerCount) {
|
|
1025
|
+
currentFrame++;
|
|
1026
|
+
} else if (loop) {
|
|
1027
|
+
currentFrame = 0;
|
|
1028
|
+
logI("Uncompressed streaming loop restart");
|
|
1029
|
+
} else {
|
|
1030
|
+
finish();
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
} catch (InterruptedException e) {
|
|
1034
|
+
Thread.currentThread().interrupt();
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
logI("Uncompressed streaming animation ended");
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
/**
|
|
1041
|
+
* Play streaming animation from KTX2 file with internal zstd compression (supercompressionScheme=2).
|
|
1042
|
+
* Only the level0 data is compressed, so the header is read directly and the layers are
|
|
1043
|
+
* decompressed as they play.
|
|
1044
|
+
*
|
|
1045
|
+
* A URL or data source is read as it decodes, without waiting: frames whose bytes haven't
|
|
1046
|
+
* arrived are simply not ready yet, and the shown frame stays.
|
|
1047
|
+
*
|
|
1048
|
+
* @param local The .ktx2 file
|
|
1049
|
+
* @param headerData The file's header, through its key/value data (level0 offset and size)
|
|
1050
|
+
*/
|
|
1051
|
+
private void playInternalZstdStreamingAnimation(AlphaVideoGpuSource local, byte[] headerData) {
|
|
1052
|
+
logI("Starting internal zstd streaming animation: " + local);
|
|
1053
|
+
|
|
1054
|
+
// Parse KTX2 header to get level0 info
|
|
1055
|
+
ByteBuffer header = ByteBuffer.wrap(headerData).order(ByteOrder.LITTLE_ENDIAN);
|
|
1056
|
+
int vkFormat = header.getInt(12);
|
|
1057
|
+
int pixelWidth = header.getInt(20);
|
|
1058
|
+
int pixelHeight = header.getInt(24);
|
|
1059
|
+
int layerCount = header.getInt(32);
|
|
1060
|
+
if (layerCount == 0) layerCount = 1;
|
|
1061
|
+
long level0Offset = header.getLong(80);
|
|
1062
|
+
long level0Length = header.getLong(88); // Compressed size
|
|
1063
|
+
long level0UncompressedLength = header.getLong(96);
|
|
1064
|
+
|
|
1065
|
+
// Configure ASTC format from vkFormat
|
|
1066
|
+
configureASTCFromVkFormat(vkFormat);
|
|
1067
|
+
|
|
1068
|
+
logI("Internal zstd: " + pixelWidth + "x" + pixelHeight + ", " + layerCount + " layers, " +
|
|
1069
|
+
"compressed=" + level0Length + ", uncompressed=" + level0UncompressedLength + ", vkFormat=" + vkFormat);
|
|
1070
|
+
|
|
1071
|
+
// Calculate bytes per layer using dynamic ASTC block size
|
|
1072
|
+
int bytesPerLayer = astcBytesPerLayer(pixelWidth, pixelHeight);
|
|
1073
|
+
|
|
1074
|
+
// Verify uncompressed size matches expected
|
|
1075
|
+
long expectedUncompressed = (long)bytesPerLayer * layerCount;
|
|
1076
|
+
if (level0UncompressedLength != expectedUncompressed) {
|
|
1077
|
+
Log.w(TAG, "Uncompressed size mismatch: header=" + level0UncompressedLength + ", expected=" + expectedUncompressed);
|
|
1078
|
+
}
|
|
1079
|
+
final ZstdSeekTable seekTable;
|
|
1080
|
+
try {
|
|
1081
|
+
seekTable = readSeekTable(local, level0Offset, level0Length, level0UncompressedLength);
|
|
1082
|
+
} catch (IOException e) {
|
|
1083
|
+
if (shouldStop.get()) return;
|
|
1084
|
+
mainHandler.post(() -> notifyError("Failed to read " + local + ": " + e.getMessage()));
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
logI(seekTable != null ? "Seek table: " + seekTable.size() + " seek points" : "No seek table: seeks decode from the start");
|
|
1088
|
+
|
|
1089
|
+
// Initialize internal zstd streaming decompression over a memory-mapped range
|
|
1090
|
+
final long[] streamId = {0};
|
|
1091
|
+
try {
|
|
1092
|
+
streamId[0] = openInternalZstdStream(local, level0Offset, level0Length);
|
|
1093
|
+
} catch (IOException e) {
|
|
1094
|
+
mainHandler.post(() -> notifyError("Failed to stream " + local + ": " + e.getMessage()));
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
if (streamId[0] == 0) {
|
|
1098
|
+
mainHandler.post(() -> notifyError("Failed to initialize internal zstd streaming"));
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
totalFrames = layerCount;
|
|
1103
|
+
frameWidth = pixelWidth;
|
|
1104
|
+
frameHeight = pixelHeight;
|
|
1105
|
+
|
|
1106
|
+
boolean useAstc = astcSupported;
|
|
1107
|
+
|
|
1108
|
+
// Determine memory strategy
|
|
1109
|
+
final boolean useVramStrategy = !"ram".equals(memoryStrategy);
|
|
1110
|
+
final int PREFETCH_AHEAD = configStreamingPrefetchAhead;
|
|
1111
|
+
// The shown frame, the ones decoded after it, and the previous frame, which the GPU may
|
|
1112
|
+
// still be drawing
|
|
1113
|
+
final int framesKept = PREFETCH_AHEAD + 2;
|
|
1114
|
+
final int GPU_CACHE_SIZE = useVramStrategy ? framesKept : RAM_RING_SIZE;
|
|
1115
|
+
final int CPU_CACHE_SIZE = useVramStrategy ? 5 : framesKept;
|
|
1116
|
+
|
|
1117
|
+
logI("Internal zstd streaming params: strategy=" + memoryStrategy +
|
|
1118
|
+
", gpuCache=" + GPU_CACHE_SIZE + ", cpuCache=" + CPU_CACHE_SIZE +
|
|
1119
|
+
", prefetch=" + PREFETCH_AHEAD);
|
|
1120
|
+
|
|
1121
|
+
// Create texture array
|
|
1122
|
+
int textureLayerCount = Math.min(GPU_CACHE_SIZE, layerCount);
|
|
1123
|
+
textureArrayId = AlphaVideoGpuNative.nativeCreateTextureArray(pixelWidth, pixelHeight, textureLayerCount, useAstc, astcGlFormat);
|
|
1124
|
+
if (textureArrayId == 0) {
|
|
1125
|
+
mainHandler.post(() -> notifyError("Failed to create texture array for internal zstd streaming"));
|
|
1126
|
+
AlphaVideoGpuNative.nativeStreamingDecompressEnd(streamId[0]);
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
textureArrayLayerCount = textureLayerCount;
|
|
1130
|
+
|
|
1131
|
+
logI("Created internal zstd streaming texture array: ID=" + textureArrayId +
|
|
1132
|
+
", " + pixelWidth + "x" + pixelHeight + ", " + textureLayerCount + " GPU slots");
|
|
1133
|
+
|
|
1134
|
+
streamingRingBufferMode = true;
|
|
1135
|
+
streamingRingSize = textureLayerCount;
|
|
1136
|
+
|
|
1137
|
+
// Initialize PBOs for async uploads (only for ASTC compressed format)
|
|
1138
|
+
if (useAstc) {
|
|
1139
|
+
pbosInitialized = AlphaVideoGpuNative.nativeInitPBOs(textureArrayId, bytesPerLayer);
|
|
1140
|
+
if (pbosInitialized) {
|
|
1141
|
+
logI("PBOs initialized for internal zstd streaming: " + bytesPerLayer + " bytes per buffer");
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
// Initialize shaders
|
|
1146
|
+
if (!initShaders()) {
|
|
1147
|
+
mainHandler.post(() -> notifyError("Failed to initialize shaders"));
|
|
1148
|
+
AlphaVideoGpuNative.nativeStreamingDecompressEnd(streamId[0]);
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
// Track ring buffer slots
|
|
1153
|
+
int[] ringSlots = new int[textureLayerCount];
|
|
1154
|
+
Arrays.fill(ringSlots, -1);
|
|
1155
|
+
|
|
1156
|
+
// Decompression state - NO header to skip since we start at level0 data directly
|
|
1157
|
+
final int[] decodePos = {0};
|
|
1158
|
+
final boolean[] decompressEnded = {false};
|
|
1159
|
+
// The last decode stopped for bytes still on their way (a URL or data source)
|
|
1160
|
+
final boolean[] inputPending = {false};
|
|
1161
|
+
// Why the stream failed (corrupt data, or a read that failed after retries), else null
|
|
1162
|
+
final String[] streamError = {null};
|
|
1163
|
+
// The frames before a seek target
|
|
1164
|
+
final long[] bytesToSkip = {0};
|
|
1165
|
+
|
|
1166
|
+
// Buffer for decompression
|
|
1167
|
+
final int bufferSize = 64 * 1024;
|
|
1168
|
+
final ByteBuffer decompressBuffer = ByteBuffer.allocateDirect(bufferSize);
|
|
1169
|
+
|
|
1170
|
+
// Frame accumulator and cache
|
|
1171
|
+
final ByteBuffer frameAccumulatorBuffer;
|
|
1172
|
+
final java.util.Queue<byte[]> bufferPool;
|
|
1173
|
+
final java.util.Map<Integer, byte[]> frameCache;
|
|
1174
|
+
|
|
1175
|
+
if (useVramStrategy) {
|
|
1176
|
+
frameAccumulatorBuffer = ByteBuffer.allocateDirect(bytesPerLayer + bufferSize);
|
|
1177
|
+
bufferPool = null;
|
|
1178
|
+
frameCache = new java.util.concurrent.ConcurrentHashMap<>();
|
|
1179
|
+
} else {
|
|
1180
|
+
// RAM mode: DirectByteBuffer for accumulation (zero-copy decompression),
|
|
1181
|
+
// byte[] pool for CPU cache storage
|
|
1182
|
+
frameAccumulatorBuffer = ByteBuffer.allocateDirect(bytesPerLayer + bufferSize);
|
|
1183
|
+
final int POOL_SIZE = CPU_CACHE_SIZE;
|
|
1184
|
+
bufferPool = new java.util.concurrent.ConcurrentLinkedQueue<>();
|
|
1185
|
+
for (int i = 0; i < POOL_SIZE; i++) {
|
|
1186
|
+
bufferPool.offer(new byte[bytesPerLayer]);
|
|
1187
|
+
}
|
|
1188
|
+
frameCache = new java.util.concurrent.ConcurrentHashMap<>();
|
|
1189
|
+
}
|
|
1190
|
+
// Both modes now use DirectByteBuffer for accumulation
|
|
1191
|
+
|
|
1192
|
+
final int finalWidth = pixelWidth;
|
|
1193
|
+
final int finalHeight = pixelHeight;
|
|
1194
|
+
final int finalBytesPerLayer = bytesPerLayer;
|
|
1195
|
+
final int finalLayerCount = layerCount;
|
|
1196
|
+
final boolean finalUseAstc = useAstc;
|
|
1197
|
+
final int finalGpuCacheSize = textureLayerCount;
|
|
1198
|
+
final long finalLevel0Offset = level0Offset;
|
|
1199
|
+
final long finalLevel0Length = level0Length;
|
|
1200
|
+
|
|
1201
|
+
announceLoaded();
|
|
1202
|
+
|
|
1203
|
+
// Track loop iterations for wrap-around prefetching
|
|
1204
|
+
final int[] decoderLoopIteration = {0};
|
|
1205
|
+
final int vPosModulus = 2 * totalFrames;
|
|
1206
|
+
|
|
1207
|
+
// Helper: Decode frames into cache
|
|
1208
|
+
java.util.function.IntConsumer decodeFrames = (virtualPlaybackPos) -> {
|
|
1209
|
+
int currentVirtualDecodePos = (decoderLoopIteration[0] * totalFrames + decodePos[0]) % vPosModulus;
|
|
1210
|
+
int framesToDecode = (currentVirtualDecodePos - virtualPlaybackPos + vPosModulus) % vPosModulus;
|
|
1211
|
+
// framesToDecode counts the shown frame
|
|
1212
|
+
int targetFramesAhead = PREFETCH_AHEAD + 1;
|
|
1213
|
+
// A long catch-up to a seek goes on over several calls, so new seeks and stops get in
|
|
1214
|
+
long skipDeadline = System.nanoTime() + SKIP_SLICE_NANOS;
|
|
1215
|
+
inputPending[0] = false;
|
|
1216
|
+
if (streamError[0] != null) return;
|
|
1217
|
+
|
|
1218
|
+
while (framesToDecode < targetFramesAhead) {
|
|
1219
|
+
// Once decompression has ended and the last frames are out of the accumulator (a
|
|
1220
|
+
// 64 KB chunk can hold several small frames), restart for the next loop
|
|
1221
|
+
boolean framesLeft = frameAccumulatorBuffer.position() >= finalBytesPerLayer;
|
|
1222
|
+
if (decompressEnded[0] && !framesLeft) {
|
|
1223
|
+
if (!loop) break;
|
|
1224
|
+
// Restart decompression for next loop
|
|
1225
|
+
decoderLoopIteration[0]++;
|
|
1226
|
+
decodePos[0] = 0;
|
|
1227
|
+
frameAccumulatorBuffer.clear(); // Both modes use DirectByteBuffer now
|
|
1228
|
+
decompressEnded[0] = !restartInternalZstdStream(streamId, local, finalLevel0Offset, finalLevel0Length, 0);
|
|
1229
|
+
if (decompressEnded[0]) break;
|
|
1230
|
+
logI("Internal zstd prefetch wrap-around: restarting for loop " + decoderLoopIteration[0]);
|
|
1231
|
+
continue;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
while (bytesToSkip[0] > 0 && !decompressEnded[0]) {
|
|
1235
|
+
if (System.nanoTime() > skipDeadline) return;
|
|
1236
|
+
decompressBuffer.clear();
|
|
1237
|
+
long result = AlphaVideoGpuNative.nativeStreamingDecompressContinueToBuffer(streamId[0], decompressBuffer, bufferSize);
|
|
1238
|
+
if (result == -1) { streamError[0] = streamFailure(local, streamId[0]); return; }
|
|
1239
|
+
int status = (int)(result >> 32);
|
|
1240
|
+
int bytesWritten = (int)(result & 0xFFFFFFFFL);
|
|
1241
|
+
if (status == AlphaVideoGpuNative.STREAM_PENDING) { inputPending[0] = true; return; }
|
|
1242
|
+
if (bytesWritten >= bytesToSkip[0]) {
|
|
1243
|
+
decompressBuffer.position((int) bytesToSkip[0]);
|
|
1244
|
+
decompressBuffer.limit(bytesWritten);
|
|
1245
|
+
frameAccumulatorBuffer.put(decompressBuffer);
|
|
1246
|
+
bytesToSkip[0] = 0;
|
|
1247
|
+
} else {
|
|
1248
|
+
bytesToSkip[0] -= bytesWritten;
|
|
1249
|
+
}
|
|
1250
|
+
if (status == AlphaVideoGpuNative.STREAM_DONE) { decompressEnded[0] = true; break; }
|
|
1251
|
+
}
|
|
1252
|
+
if (decompressEnded[0] && frameAccumulatorBuffer.position() < finalBytesPerLayer) continue;
|
|
1253
|
+
|
|
1254
|
+
int accumulatorSize = frameAccumulatorBuffer.position();
|
|
1255
|
+
|
|
1256
|
+
// Decompress until we have a full frame
|
|
1257
|
+
while (accumulatorSize < finalBytesPerLayer && !decompressEnded[0]) {
|
|
1258
|
+
decompressBuffer.clear();
|
|
1259
|
+
long result = AlphaVideoGpuNative.nativeStreamingDecompressContinueToBuffer(streamId[0], decompressBuffer, bufferSize);
|
|
1260
|
+
if (result == -1) { streamError[0] = streamFailure(local, streamId[0]); return; }
|
|
1261
|
+
int status = (int)(result >> 32);
|
|
1262
|
+
int bytesWritten = (int)(result & 0xFFFFFFFFL);
|
|
1263
|
+
if (status == AlphaVideoGpuNative.STREAM_PENDING) { inputPending[0] = true; return; }
|
|
1264
|
+
if (bytesWritten > 0) {
|
|
1265
|
+
decompressBuffer.limit(bytesWritten);
|
|
1266
|
+
decompressBuffer.position(0);
|
|
1267
|
+
// Zero-copy: transfer directly between ByteBuffers (both modes)
|
|
1268
|
+
frameAccumulatorBuffer.put(decompressBuffer);
|
|
1269
|
+
accumulatorSize += bytesWritten;
|
|
1270
|
+
}
|
|
1271
|
+
if (status == AlphaVideoGpuNative.STREAM_DONE) { decompressEnded[0] = true; break; }
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
if (accumulatorSize >= finalBytesPerLayer) {
|
|
1275
|
+
int virtualPos = (decoderLoopIteration[0] * totalFrames + decodePos[0]) % vPosModulus;
|
|
1276
|
+
int actualFrame = decodePos[0];
|
|
1277
|
+
decodePos[0]++;
|
|
1278
|
+
|
|
1279
|
+
if (useVramStrategy) {
|
|
1280
|
+
int gpuSlot = actualFrame % finalGpuCacheSize;
|
|
1281
|
+
int slotCurrentVPos = ringSlots[gpuSlot];
|
|
1282
|
+
int slotAge = (virtualPlaybackPos - slotCurrentVPos + vPosModulus) % vPosModulus;
|
|
1283
|
+
boolean slotIsOld = slotAge > 0 && slotAge < vPosModulus / 2;
|
|
1284
|
+
boolean canOverwrite = (slotCurrentVPos == -1) || (slotCurrentVPos == virtualPos) || slotIsOld;
|
|
1285
|
+
|
|
1286
|
+
if (canOverwrite && ringSlots[gpuSlot] != virtualPos) {
|
|
1287
|
+
frameAccumulatorBuffer.flip();
|
|
1288
|
+
// Use PBO for async upload when available
|
|
1289
|
+
boolean uploaded = pbosInitialized ?
|
|
1290
|
+
AlphaVideoGpuNative.nativeUploadTextureArrayLayerPBO(
|
|
1291
|
+
textureArrayId, gpuSlot, finalWidth, finalHeight,
|
|
1292
|
+
frameAccumulatorBuffer, finalBytesPerLayer, finalUseAstc,
|
|
1293
|
+
astcGlFormat, astcBlockWidth, astcBlockHeight) :
|
|
1294
|
+
AlphaVideoGpuNative.nativeUploadTextureArrayLayerFromBuffer(
|
|
1295
|
+
textureArrayId, gpuSlot, finalWidth, finalHeight,
|
|
1296
|
+
frameAccumulatorBuffer, finalBytesPerLayer, finalUseAstc,
|
|
1297
|
+
astcGlFormat, astcBlockWidth, astcBlockHeight);
|
|
1298
|
+
if (uploaded) {
|
|
1299
|
+
ringSlots[gpuSlot] = virtualPos;
|
|
1300
|
+
}
|
|
1301
|
+
if (frameAccumulatorBuffer.remaining() > finalBytesPerLayer) {
|
|
1302
|
+
frameAccumulatorBuffer.position(finalBytesPerLayer);
|
|
1303
|
+
frameAccumulatorBuffer.compact();
|
|
1304
|
+
} else {
|
|
1305
|
+
frameAccumulatorBuffer.clear();
|
|
1306
|
+
}
|
|
1307
|
+
} else {
|
|
1308
|
+
byte[] tempData = new byte[finalBytesPerLayer];
|
|
1309
|
+
frameAccumulatorBuffer.flip();
|
|
1310
|
+
frameAccumulatorBuffer.get(tempData, 0, finalBytesPerLayer);
|
|
1311
|
+
frameCache.put(virtualPos, tempData);
|
|
1312
|
+
if (frameAccumulatorBuffer.remaining() > 0) {
|
|
1313
|
+
frameAccumulatorBuffer.compact();
|
|
1314
|
+
} else {
|
|
1315
|
+
frameAccumulatorBuffer.clear();
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
} else {
|
|
1319
|
+
// RAM mode: Copy from DirectByteBuffer to pooled byte[] for CPU cache
|
|
1320
|
+
byte[] frameData = bufferPool.poll();
|
|
1321
|
+
if (frameData == null) {
|
|
1322
|
+
frameData = new byte[finalBytesPerLayer];
|
|
1323
|
+
}
|
|
1324
|
+
// Single copy: DirectByteBuffer -> pooled byte[]
|
|
1325
|
+
frameAccumulatorBuffer.flip();
|
|
1326
|
+
frameAccumulatorBuffer.get(frameData, 0, finalBytesPerLayer);
|
|
1327
|
+
frameCache.put(virtualPos, frameData);
|
|
1328
|
+
// Keep remaining data in accumulator
|
|
1329
|
+
if (frameAccumulatorBuffer.remaining() > 0) {
|
|
1330
|
+
frameAccumulatorBuffer.compact();
|
|
1331
|
+
} else {
|
|
1332
|
+
frameAccumulatorBuffer.clear();
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
int newDecodePos = (decoderLoopIteration[0] * totalFrames + decodePos[0]) % vPosModulus;
|
|
1337
|
+
framesToDecode = (newDecodePos - virtualPlaybackPos + vPosModulus) % vPosModulus;
|
|
1338
|
+
} else if (!decompressEnded[0]) {
|
|
1339
|
+
break;
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
};
|
|
1343
|
+
|
|
1344
|
+
// Helper: Evict old frames
|
|
1345
|
+
java.util.function.IntConsumer evictOldFrames = (virtualPlaybackPos) -> {
|
|
1346
|
+
java.util.List<Integer> toRemove = new java.util.ArrayList<>();
|
|
1347
|
+
if (useVramStrategy) {
|
|
1348
|
+
for (Integer virtualPos : frameCache.keySet()) {
|
|
1349
|
+
int age = (virtualPlaybackPos - virtualPos + vPosModulus) % vPosModulus;
|
|
1350
|
+
if (age > 0 && age < vPosModulus / 2) {
|
|
1351
|
+
toRemove.add(virtualPos);
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
} else {
|
|
1355
|
+
for (Integer virtualPos : frameCache.keySet()) {
|
|
1356
|
+
int distance = (virtualPos - virtualPlaybackPos + vPosModulus) % vPosModulus;
|
|
1357
|
+
boolean isBehind = distance > vPosModulus - 2;
|
|
1358
|
+
boolean isAhead = distance <= CPU_CACHE_SIZE;
|
|
1359
|
+
if (!isBehind && !isAhead) {
|
|
1360
|
+
toRemove.add(virtualPos);
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
for (Integer virtualPos : toRemove) {
|
|
1365
|
+
byte[] buffer = frameCache.remove(virtualPos);
|
|
1366
|
+
if (buffer != null && bufferPool != null && buffer.length == finalBytesPerLayer) {
|
|
1367
|
+
bufferPool.offer(buffer);
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
};
|
|
1371
|
+
|
|
1372
|
+
// Helper: Ensure frame in GPU
|
|
1373
|
+
java.util.function.IntConsumer ensureFrameInGpu = (virtualPos) -> {
|
|
1374
|
+
int actualFrame = virtualPos % totalFrames;
|
|
1375
|
+
int gpuSlot = actualFrame % finalGpuCacheSize;
|
|
1376
|
+
if (ringSlots[gpuSlot] == virtualPos) return;
|
|
1377
|
+
byte[] frameData = frameCache.get(virtualPos);
|
|
1378
|
+
if (frameData == null) return;
|
|
1379
|
+
boolean uploaded = AlphaVideoGpuNative.nativeUploadTextureArrayLayer(
|
|
1380
|
+
textureArrayId, gpuSlot, finalWidth, finalHeight, frameData, finalUseAstc,
|
|
1381
|
+
astcGlFormat, astcBlockWidth, astcBlockHeight);
|
|
1382
|
+
if (uploaded) {
|
|
1383
|
+
ringSlots[gpuSlot] = virtualPos;
|
|
1384
|
+
if (useVramStrategy) {
|
|
1385
|
+
frameCache.remove(virtualPos);
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
};
|
|
1389
|
+
|
|
1390
|
+
// Main playback loop
|
|
1391
|
+
FrameClock clock = new FrameClock(frameIntervalNanos());
|
|
1392
|
+
// Only its parity matters: virtual positions are taken modulo 2 * totalFrames
|
|
1393
|
+
int playbackLoopIteration = 0;
|
|
1394
|
+
currentFrame = 0;
|
|
1395
|
+
boolean frameOwed = true;
|
|
1396
|
+
boolean wasPlaying = false;
|
|
1397
|
+
// Per pass, for tuning streamingPrefetchAhead: the fewest frames decoded after the shown
|
|
1398
|
+
// one, and how often playback waited for the decoder
|
|
1399
|
+
int fewestAhead = totalFrames;
|
|
1400
|
+
int stalls = 0;
|
|
1401
|
+
boolean stalled = false;
|
|
1402
|
+
|
|
1403
|
+
try {
|
|
1404
|
+
while (!shouldStop.get() && !isDestroyed.get()) {
|
|
1405
|
+
int seek = takeSeek();
|
|
1406
|
+
if (seek >= 0) {
|
|
1407
|
+
boolean samePass = decoderLoopIteration[0] % 2 == playbackLoopIteration % 2;
|
|
1408
|
+
// Frames from the shown one up to the decoder are buffered (in the cache or the ring)
|
|
1409
|
+
boolean buffered = samePass && seek >= currentFrame && seek < decodePos[0];
|
|
1410
|
+
if (!buffered) {
|
|
1411
|
+
// Carry on decoding when that is no further from the target than the
|
|
1412
|
+
// nearest seek point (the level start, without a seek table)
|
|
1413
|
+
long target = (long) seek * finalBytesPerLayer;
|
|
1414
|
+
long[] seekPoint = ZstdSeekTable.seekPointBefore(seekTable, target);
|
|
1415
|
+
long decoded = (long) decodePos[0] * finalBytesPerLayer
|
|
1416
|
+
+ frameAccumulatorBuffer.position() - bytesToSkip[0];
|
|
1417
|
+
if (samePass && seek >= decodePos[0] && !decompressEnded[0] && decoded >= seekPoint[1]) {
|
|
1418
|
+
skipDecodedBytes(frameAccumulatorBuffer, bytesToSkip,
|
|
1419
|
+
(long) (seek - decodePos[0]) * finalBytesPerLayer);
|
|
1420
|
+
} else {
|
|
1421
|
+
decompressEnded[0] = !restartInternalZstdStream(
|
|
1422
|
+
streamId, local, finalLevel0Offset, finalLevel0Length, seekPoint[0]);
|
|
1423
|
+
frameAccumulatorBuffer.clear();
|
|
1424
|
+
bytesToSkip[0] = target - seekPoint[1];
|
|
1425
|
+
}
|
|
1426
|
+
decodePos[0] = seek;
|
|
1427
|
+
decoderLoopIteration[0] = 0;
|
|
1428
|
+
playbackLoopIteration = 0;
|
|
1429
|
+
dropStreamedFrames(frameCache, bufferPool, ringSlots, finalBytesPerLayer);
|
|
1430
|
+
}
|
|
1431
|
+
currentFrame = seek;
|
|
1432
|
+
frameOwed = true;
|
|
1433
|
+
wasPlaying = false;
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
boolean playing = isPlaying.get();
|
|
1437
|
+
if (!playing && !frameOwed) {
|
|
1438
|
+
wasPlaying = false;
|
|
1439
|
+
Thread.sleep(16);
|
|
1440
|
+
continue;
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
int virtualPlaybackPos = (playbackLoopIteration * totalFrames + currentFrame) % vPosModulus;
|
|
1444
|
+
decodeFrames.accept(virtualPlaybackPos);
|
|
1445
|
+
if (streamError[0] != null) {
|
|
1446
|
+
final String error = streamError[0];
|
|
1447
|
+
mainHandler.post(() -> notifyError(error));
|
|
1448
|
+
break;
|
|
1449
|
+
}
|
|
1450
|
+
evictOldFrames.accept(virtualPlaybackPos);
|
|
1451
|
+
|
|
1452
|
+
int currentGpuSlot = currentFrame % finalGpuCacheSize;
|
|
1453
|
+
boolean frameInGpu = ringSlots[currentGpuSlot] == virtualPlaybackPos;
|
|
1454
|
+
boolean frameInCpuCache = frameCache.containsKey(virtualPlaybackPos);
|
|
1455
|
+
if (!frameInGpu && !frameInCpuCache) {
|
|
1456
|
+
// Still catching up to a seek: decodeFrames goes on with it
|
|
1457
|
+
if (bytesToSkip[0] > 0 && !decompressEnded[0] && !inputPending[0]) continue;
|
|
1458
|
+
if (playing && !stalled) {
|
|
1459
|
+
stalled = true;
|
|
1460
|
+
stalls++;
|
|
1461
|
+
}
|
|
1462
|
+
// Bytes on their way are expected; anything else is a stall worth a warning
|
|
1463
|
+
if (!inputPending[0]) {
|
|
1464
|
+
Log.w(TAG, "Internal zstd frame not ready: frame=" + currentFrame + ", vPos=" + virtualPlaybackPos);
|
|
1465
|
+
}
|
|
1466
|
+
Thread.sleep(8);
|
|
1467
|
+
continue;
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
ensureFrameInGpu.accept(virtualPlaybackPos);
|
|
1471
|
+
if (!useVramStrategy) {
|
|
1472
|
+
for (int i = 1; i <= 2; i++) {
|
|
1473
|
+
int nextVirtualPos = (virtualPlaybackPos + i) % vPosModulus;
|
|
1474
|
+
int nextGpuSlot = ((currentFrame + i) % totalFrames) % finalGpuCacheSize;
|
|
1475
|
+
// Past the loop point a frame can map to the shown frame's slot
|
|
1476
|
+
if (nextGpuSlot == currentGpuSlot) break;
|
|
1477
|
+
if (frameCache.containsKey(nextVirtualPos) || ringSlots[nextGpuSlot] == nextVirtualPos) {
|
|
1478
|
+
ensureFrameInGpu.accept(nextVirtualPos);
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
renderFrame();
|
|
1484
|
+
frameOwed = false;
|
|
1485
|
+
if (!playing) {
|
|
1486
|
+
wasPlaying = false;
|
|
1487
|
+
continue;
|
|
1488
|
+
}
|
|
1489
|
+
if (!wasPlaying) {
|
|
1490
|
+
clock.restart();
|
|
1491
|
+
wasPlaying = true;
|
|
1492
|
+
}
|
|
1493
|
+
stalled = false;
|
|
1494
|
+
int decodedVPos = (decoderLoopIteration[0] * totalFrames + decodePos[0]) % vPosModulus;
|
|
1495
|
+
int ahead = (decodedVPos - virtualPlaybackPos + vPosModulus) % vPosModulus - 1;
|
|
1496
|
+
fewestAhead = Math.min(fewestAhead, ahead);
|
|
1497
|
+
if (enableFrameEvents) {
|
|
1498
|
+
mainHandler.post(() -> notifyFrameUpdate());
|
|
1499
|
+
}
|
|
1500
|
+
clock.awaitNext();
|
|
1501
|
+
|
|
1502
|
+
if (currentFrame + 1 < totalFrames) {
|
|
1503
|
+
currentFrame++;
|
|
1504
|
+
} else {
|
|
1505
|
+
logI("Pass buffer: fewest frames ahead " + fewestAhead + " of " + PREFETCH_AHEAD
|
|
1506
|
+
+ ", stalls " + stalls);
|
|
1507
|
+
fewestAhead = totalFrames;
|
|
1508
|
+
stalls = 0;
|
|
1509
|
+
if (loop) {
|
|
1510
|
+
currentFrame = 0;
|
|
1511
|
+
playbackLoopIteration = (playbackLoopIteration + 1) % 2;
|
|
1512
|
+
logI("Internal zstd loop transition: playbackLoop=" + playbackLoopIteration);
|
|
1513
|
+
} else {
|
|
1514
|
+
finish();
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
} catch (InterruptedException e) {
|
|
1519
|
+
Thread.currentThread().interrupt();
|
|
1520
|
+
} finally {
|
|
1521
|
+
AlphaVideoGpuNative.nativeStreamingDecompressEnd(streamId[0]);
|
|
1522
|
+
}
|
|
1523
|
+
logI("Internal zstd streaming animation ended");
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
private boolean initGL(SurfaceTexture surfaceTexture) {
|
|
1527
|
+
eglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
|
|
1528
|
+
if (eglDisplay == EGL14.EGL_NO_DISPLAY) {
|
|
1529
|
+
Log.e(TAG, "Unable to get EGL display");
|
|
1530
|
+
return false;
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
int[] version = new int[2];
|
|
1534
|
+
if (!EGL14.eglInitialize(eglDisplay, version, 0, version, 1)) {
|
|
1535
|
+
Log.e(TAG, "Unable to initialize EGL");
|
|
1536
|
+
return false;
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
// EGL_OPENGL_ES3_BIT_KHR = 0x40 (not available in all SDK levels)
|
|
1540
|
+
int[] configAttribs = {
|
|
1541
|
+
EGL14.EGL_RED_SIZE, 8,
|
|
1542
|
+
EGL14.EGL_GREEN_SIZE, 8,
|
|
1543
|
+
EGL14.EGL_BLUE_SIZE, 8,
|
|
1544
|
+
EGL14.EGL_ALPHA_SIZE, 8,
|
|
1545
|
+
EGL14.EGL_RENDERABLE_TYPE, 0x40, // EGL_OPENGL_ES3_BIT_KHR
|
|
1546
|
+
EGL14.EGL_SURFACE_TYPE, EGL14.EGL_WINDOW_BIT,
|
|
1547
|
+
EGL14.EGL_NONE
|
|
1548
|
+
};
|
|
1549
|
+
|
|
1550
|
+
EGLConfig[] configs = new EGLConfig[1];
|
|
1551
|
+
int[] numConfigs = new int[1];
|
|
1552
|
+
if (!EGL14.eglChooseConfig(eglDisplay, configAttribs, 0, configs, 0, 1, numConfigs, 0)) {
|
|
1553
|
+
Log.e(TAG, "Unable to choose EGL config");
|
|
1554
|
+
return false;
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
int[] contextAttribs = {
|
|
1558
|
+
EGL14.EGL_CONTEXT_CLIENT_VERSION, 3,
|
|
1559
|
+
EGL14.EGL_NONE
|
|
1560
|
+
};
|
|
1561
|
+
eglContext = EGL14.eglCreateContext(eglDisplay, configs[0], EGL14.EGL_NO_CONTEXT, contextAttribs, 0);
|
|
1562
|
+
if (eglContext == EGL14.EGL_NO_CONTEXT) {
|
|
1563
|
+
Log.e(TAG, "Unable to create EGL context");
|
|
1564
|
+
return false;
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
int[] surfaceAttribs = { EGL14.EGL_NONE };
|
|
1568
|
+
eglSurface = EGL14.eglCreateWindowSurface(eglDisplay, configs[0], surfaceTexture, surfaceAttribs, 0);
|
|
1569
|
+
if (eglSurface == EGL14.EGL_NO_SURFACE) {
|
|
1570
|
+
Log.e(TAG, "Unable to create EGL surface");
|
|
1571
|
+
return false;
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
if (!EGL14.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext)) {
|
|
1575
|
+
Log.e(TAG, "Unable to make EGL context current");
|
|
1576
|
+
return false;
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
// Enable blending for transparency
|
|
1580
|
+
// Use premultiplied alpha blending (RGB is pre-multiplied by alpha in encoder)
|
|
1581
|
+
GLES30.glEnable(GLES30.GL_BLEND);
|
|
1582
|
+
GLES30.glBlendFunc(GLES30.GL_ONE, GLES30.GL_ONE_MINUS_SRC_ALPHA);
|
|
1583
|
+
|
|
1584
|
+
logD("OpenGL ES 3.0 initialized successfully");
|
|
1585
|
+
return true;
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
private boolean detectASTCSupport() {
|
|
1589
|
+
String extensions = GLES30.glGetString(GLES30.GL_EXTENSIONS);
|
|
1590
|
+
if (extensions != null && extensions.contains("GL_KHR_texture_compression_astc_ldr")) {
|
|
1591
|
+
logI("The GPU decodes ASTC");
|
|
1592
|
+
return true;
|
|
1593
|
+
}
|
|
1594
|
+
Log.w(TAG, "No GPU ASTC support: decoding frames to RGBA in software");
|
|
1595
|
+
return false;
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
private boolean initShaders() {
|
|
1599
|
+
int vertexShader = compileShader(GLES30.GL_VERTEX_SHADER, TEXTURE_ARRAY_VERTEX_SHADER);
|
|
1600
|
+
if (vertexShader == 0) {
|
|
1601
|
+
Log.e(TAG, "Failed to compile texture array vertex shader");
|
|
1602
|
+
return false;
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
int fragmentShader = compileShader(GLES30.GL_FRAGMENT_SHADER, TEXTURE_ARRAY_FRAGMENT_SHADER);
|
|
1606
|
+
if (fragmentShader == 0) {
|
|
1607
|
+
GLES30.glDeleteShader(vertexShader);
|
|
1608
|
+
Log.e(TAG, "Failed to compile texture array fragment shader");
|
|
1609
|
+
return false;
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
arrayProgram = GLES30.glCreateProgram();
|
|
1613
|
+
GLES30.glAttachShader(arrayProgram, vertexShader);
|
|
1614
|
+
GLES30.glAttachShader(arrayProgram, fragmentShader);
|
|
1615
|
+
GLES30.glLinkProgram(arrayProgram);
|
|
1616
|
+
|
|
1617
|
+
int[] linkStatus = new int[1];
|
|
1618
|
+
GLES30.glGetProgramiv(arrayProgram, GLES30.GL_LINK_STATUS, linkStatus, 0);
|
|
1619
|
+
if (linkStatus[0] != GLES30.GL_TRUE) {
|
|
1620
|
+
Log.e(TAG, "Texture array program link error: " + GLES30.glGetProgramInfoLog(arrayProgram));
|
|
1621
|
+
GLES30.glDeleteProgram(arrayProgram);
|
|
1622
|
+
arrayProgram = 0;
|
|
1623
|
+
return false;
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
GLES30.glDeleteShader(vertexShader);
|
|
1627
|
+
GLES30.glDeleteShader(fragmentShader);
|
|
1628
|
+
|
|
1629
|
+
arrayUniformLayerLocation = GLES30.glGetUniformLocation(arrayProgram, "uLayer");
|
|
1630
|
+
logD("Texture array shader initialized (uLayer location: " + arrayUniformLayerLocation + ")");
|
|
1631
|
+
return true;
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
private int compileShader(int type, String source) {
|
|
1635
|
+
int shader = GLES30.glCreateShader(type);
|
|
1636
|
+
GLES30.glShaderSource(shader, source);
|
|
1637
|
+
GLES30.glCompileShader(shader);
|
|
1638
|
+
|
|
1639
|
+
int[] compileStatus = new int[1];
|
|
1640
|
+
GLES30.glGetShaderiv(shader, GLES30.GL_COMPILE_STATUS, compileStatus, 0);
|
|
1641
|
+
if (compileStatus[0] != GLES30.GL_TRUE) {
|
|
1642
|
+
Log.e(TAG, "Shader compile error: " + GLES30.glGetShaderInfoLog(shader));
|
|
1643
|
+
GLES30.glDeleteShader(shader);
|
|
1644
|
+
return 0;
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
return shader;
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
/** Takes the frame size, count and rate from the file; returns an error unless it plays. */
|
|
1651
|
+
private String applyMetadata(Ktx2Metadata metadata) {
|
|
1652
|
+
if (metadata == null) return "Not a KTX2 file";
|
|
1653
|
+
if (!metadata.isAstcArray()) {
|
|
1654
|
+
return "Only ASTC texture arrays play (this file has vkFormat " + metadata.vkFormat + " and "
|
|
1655
|
+
+ metadata.frameCount + " layers); encode it with texenc";
|
|
1656
|
+
}
|
|
1657
|
+
frameWidth = metadata.width;
|
|
1658
|
+
frameHeight = metadata.height;
|
|
1659
|
+
totalFrames = metadata.frameCount;
|
|
1660
|
+
textureArrayLayerCount = metadata.frameCount;
|
|
1661
|
+
frameRate = metadata.frameRate;
|
|
1662
|
+
logI("Metadata: " + totalFrames + " frames, " + frameWidth + "x" + frameHeight + ", " + frameRate + " fps");
|
|
1663
|
+
return null;
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
/**
|
|
1667
|
+
* Get supercompression scheme from KTX2 header.
|
|
1668
|
+
* Returns 0 for uncompressed, 2 for Zstd, or -1 on error.
|
|
1669
|
+
*/
|
|
1670
|
+
private int getSupercompressionScheme(byte[] ktx2Data) {
|
|
1671
|
+
if (ktx2Data == null || ktx2Data.length < 48) return -1;
|
|
1672
|
+
ByteBuffer header = ByteBuffer.wrap(ktx2Data).order(ByteOrder.LITTLE_ENDIAN);
|
|
1673
|
+
return header.getInt(44);
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
/**
|
|
1677
|
+
* Configure ASTC block dimensions and GL format from a KTX2 vkFormat value.
|
|
1678
|
+
* VkFormat 157-184 are ASTC UNORM/SRGB pairs for all 14 block sizes.
|
|
1679
|
+
*/
|
|
1680
|
+
private void configureASTCFromVkFormat(int vkFormat) {
|
|
1681
|
+
// Normalize to base (UNORM) value
|
|
1682
|
+
int base = (vkFormat % 2 == 0) ? vkFormat - 1 : vkFormat;
|
|
1683
|
+
switch (base) {
|
|
1684
|
+
case 157: astcBlockWidth = 4; astcBlockHeight = 4; astcGlFormat = GL_COMPRESSED_RGBA_ASTC_4x4_KHR; break;
|
|
1685
|
+
case 159: astcBlockWidth = 5; astcBlockHeight = 4; astcGlFormat = GL_COMPRESSED_RGBA_ASTC_5x4_KHR; break;
|
|
1686
|
+
case 161: astcBlockWidth = 5; astcBlockHeight = 5; astcGlFormat = GL_COMPRESSED_RGBA_ASTC_5x5_KHR; break;
|
|
1687
|
+
case 163: astcBlockWidth = 6; astcBlockHeight = 5; astcGlFormat = GL_COMPRESSED_RGBA_ASTC_6x5_KHR; break;
|
|
1688
|
+
case 165: astcBlockWidth = 6; astcBlockHeight = 6; astcGlFormat = GL_COMPRESSED_RGBA_ASTC_6x6_KHR; break;
|
|
1689
|
+
case 167: astcBlockWidth = 8; astcBlockHeight = 5; astcGlFormat = GL_COMPRESSED_RGBA_ASTC_8x5_KHR; break;
|
|
1690
|
+
case 169: astcBlockWidth = 8; astcBlockHeight = 6; astcGlFormat = GL_COMPRESSED_RGBA_ASTC_8x6_KHR; break;
|
|
1691
|
+
case 171: astcBlockWidth = 8; astcBlockHeight = 8; astcGlFormat = GL_COMPRESSED_RGBA_ASTC_8x8_KHR; break;
|
|
1692
|
+
case 173: astcBlockWidth = 10; astcBlockHeight = 5; astcGlFormat = GL_COMPRESSED_RGBA_ASTC_10x5_KHR; break;
|
|
1693
|
+
case 175: astcBlockWidth = 10; astcBlockHeight = 6; astcGlFormat = GL_COMPRESSED_RGBA_ASTC_10x6_KHR; break;
|
|
1694
|
+
case 177: astcBlockWidth = 10; astcBlockHeight = 8; astcGlFormat = GL_COMPRESSED_RGBA_ASTC_10x8_KHR; break;
|
|
1695
|
+
case 179: astcBlockWidth = 10; astcBlockHeight = 10; astcGlFormat = GL_COMPRESSED_RGBA_ASTC_10x10_KHR; break;
|
|
1696
|
+
case 181: astcBlockWidth = 12; astcBlockHeight = 10; astcGlFormat = GL_COMPRESSED_RGBA_ASTC_12x10_KHR; break;
|
|
1697
|
+
case 183: astcBlockWidth = 12; astcBlockHeight = 12; astcGlFormat = GL_COMPRESSED_RGBA_ASTC_12x12_KHR; break;
|
|
1698
|
+
default: astcBlockWidth = 4; astcBlockHeight = 4; astcGlFormat = GL_COMPRESSED_RGBA_ASTC_4x4_KHR; break;
|
|
1699
|
+
}
|
|
1700
|
+
logI("ASTC format: " + astcBlockWidth + "x" + astcBlockHeight + " (vkFormat=" + vkFormat + ", glFormat=0x" + Integer.toHexString(astcGlFormat) + ")");
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
/**
|
|
1704
|
+
* Calculate bytes per layer for the current ASTC block size.
|
|
1705
|
+
* All ASTC blocks are 16 bytes (128 bits) regardless of block dimensions.
|
|
1706
|
+
*/
|
|
1707
|
+
private int astcBytesPerLayer(int width, int height) {
|
|
1708
|
+
int blocksX = (width + astcBlockWidth - 1) / astcBlockWidth;
|
|
1709
|
+
int blocksY = (height + astcBlockHeight - 1) / astcBlockHeight;
|
|
1710
|
+
return blocksX * blocksY * 16;
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
/**
|
|
1714
|
+
* Load texture array to RAM without GPU upload (for RAM-heavy mode).
|
|
1715
|
+
* Returns Object[] = {width, height, layerCount, bytesPerLayer, ByteBuffer frameData}
|
|
1716
|
+
* Uses a single contiguous direct ByteBuffer for zero-copy GPU uploads.
|
|
1717
|
+
*/
|
|
1718
|
+
private Object[] loadTextureArrayToRam(ByteBuffer ktx2Data, boolean useAstc) {
|
|
1719
|
+
try {
|
|
1720
|
+
// Parse KTX2 header to get dimensions and layer count
|
|
1721
|
+
if (ktx2Data.capacity() < 104) {
|
|
1722
|
+
Log.e(TAG, "KTX2 data too small for header");
|
|
1723
|
+
return null;
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
ByteBuffer header = ktx2Data.duplicate().order(ByteOrder.LITTLE_ENDIAN);
|
|
1727
|
+
|
|
1728
|
+
// Check magic
|
|
1729
|
+
if (header.get(0) != (byte)0xAB || header.get(1) != 0x4B) {
|
|
1730
|
+
Log.e(TAG, "Invalid KTX2 magic");
|
|
1731
|
+
return null;
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
int vkFormat = header.getInt(12);
|
|
1735
|
+
int pixelWidth = header.getInt(20);
|
|
1736
|
+
int pixelHeight = header.getInt(24);
|
|
1737
|
+
int layerCount = header.getInt(32);
|
|
1738
|
+
if (layerCount == 0) layerCount = 1;
|
|
1739
|
+
int supercompressionScheme = header.getInt(44); // Offset 44, not 48
|
|
1740
|
+
|
|
1741
|
+
// Configure ASTC format from vkFormat
|
|
1742
|
+
configureASTCFromVkFormat(vkFormat);
|
|
1743
|
+
|
|
1744
|
+
// Get level 0 data offset and length
|
|
1745
|
+
long level0ByteOffset = header.getLong(80);
|
|
1746
|
+
long level0ByteLength = header.getLong(88);
|
|
1747
|
+
|
|
1748
|
+
// Calculate ASTC frame size using dynamic block dimensions
|
|
1749
|
+
int bytesPerLayer = astcBytesPerLayer(pixelWidth, pixelHeight);
|
|
1750
|
+
int totalSize = bytesPerLayer * layerCount;
|
|
1751
|
+
|
|
1752
|
+
logD("KTX2 header: " + pixelWidth + "x" + pixelHeight + ", " + layerCount +
|
|
1753
|
+
" layers, supercompression=" + supercompressionScheme);
|
|
1754
|
+
|
|
1755
|
+
// Allocate direct ByteBuffer for zero-copy GPU uploads
|
|
1756
|
+
ByteBuffer frameBuffer = ByteBuffer.allocateDirect(totalSize);
|
|
1757
|
+
frameBuffer.order(ByteOrder.nativeOrder());
|
|
1758
|
+
|
|
1759
|
+
if (supercompressionScheme == 2) { // Zstd
|
|
1760
|
+
// Decompress directly into the direct ByteBuffer
|
|
1761
|
+
boolean success = AlphaVideoGpuNative.nativeDecompressZstdToBuffer(
|
|
1762
|
+
ktx2Data, level0ByteOffset, level0ByteLength, frameBuffer, totalSize);
|
|
1763
|
+
if (!success) {
|
|
1764
|
+
Log.e(TAG, "Zstd decompression to buffer failed");
|
|
1765
|
+
return null;
|
|
1766
|
+
}
|
|
1767
|
+
} else if (supercompressionScheme == 0) { // None
|
|
1768
|
+
// Copy uncompressed data directly to buffer
|
|
1769
|
+
ByteBuffer level = ktx2Data.duplicate();
|
|
1770
|
+
level.limit((int) (level0ByteOffset + level0ByteLength)).position((int) level0ByteOffset);
|
|
1771
|
+
frameBuffer.put(level);
|
|
1772
|
+
frameBuffer.rewind();
|
|
1773
|
+
} else {
|
|
1774
|
+
Log.e(TAG, "Unsupported supercompression: " + supercompressionScheme);
|
|
1775
|
+
return null;
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
logD("Decoded " + layerCount + " frames to direct ByteBuffer, " +
|
|
1779
|
+
bytesPerLayer + " bytes each, total " + (totalSize / 1024) + " KB");
|
|
1780
|
+
|
|
1781
|
+
return new Object[] { pixelWidth, pixelHeight, layerCount, bytesPerLayer, frameBuffer };
|
|
1782
|
+
} catch (Exception e) {
|
|
1783
|
+
Log.e(TAG, "Failed to load texture array to RAM: " + e.getMessage());
|
|
1784
|
+
return null;
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
private void loadTextureArray(AlphaVideoGpuSource local) throws Exception {
|
|
1789
|
+
logD("Loading texture array from: " + local);
|
|
1790
|
+
|
|
1791
|
+
// Off the Java heap, whose cap (256 MB on many phones) a large clip can pass
|
|
1792
|
+
ByteBuffer ktx2Data = local.readAllDirect(getContext());
|
|
1793
|
+
|
|
1794
|
+
boolean useAstc = astcSupported;
|
|
1795
|
+
|
|
1796
|
+
// Check memory strategy for non-stream texture array mode
|
|
1797
|
+
// "ram" mode: Decode to RAM, upload per-frame (lower VRAM usage)
|
|
1798
|
+
// "vram" mode: Upload all to GPU
|
|
1799
|
+
boolean useRamMode = "ram".equals(memoryStrategy);
|
|
1800
|
+
|
|
1801
|
+
if (useRamMode) {
|
|
1802
|
+
logD("RAM-heavy mode: Decoding texture array to RAM, will upload per-frame");
|
|
1803
|
+
|
|
1804
|
+
// Decode all frames to RAM using nativeLoadTextureArrayToRam
|
|
1805
|
+
// This returns frame data in a contiguous direct ByteBuffer (zero-copy)
|
|
1806
|
+
Object[] ramResult = loadTextureArrayToRam(ktx2Data, useAstc);
|
|
1807
|
+
if (ramResult == null) {
|
|
1808
|
+
throw new Exception("Failed to decode texture array to RAM");
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
int texWidth = (Integer) ramResult[0];
|
|
1812
|
+
int texHeight = (Integer) ramResult[1];
|
|
1813
|
+
int layerCount = (Integer) ramResult[2];
|
|
1814
|
+
ramBytesPerFrame = (Integer) ramResult[3];
|
|
1815
|
+
ramFrameBuffer = (ByteBuffer) ramResult[4];
|
|
1816
|
+
ramFrameCount = layerCount;
|
|
1817
|
+
|
|
1818
|
+
// Create small ring buffer on GPU (2 frames for double buffering)
|
|
1819
|
+
int gpuRingSize = 2;
|
|
1820
|
+
textureArrayId = AlphaVideoGpuNative.nativeCreateTextureArray(texWidth, texHeight, gpuRingSize, useAstc, astcGlFormat);
|
|
1821
|
+
if (textureArrayId == 0) {
|
|
1822
|
+
throw new Exception("Failed to create GPU ring buffer for RAM mode");
|
|
1823
|
+
}
|
|
1824
|
+
textureArrayLayerCount = gpuRingSize;
|
|
1825
|
+
ramHeavyMode = true;
|
|
1826
|
+
ramUploadedFrame = -1;
|
|
1827
|
+
|
|
1828
|
+
logD("RAM-heavy texture array: " + layerCount + " frames in direct ByteBuffer (" +
|
|
1829
|
+
(ramFrameCount * ramBytesPerFrame / 1024) + " KB), " +
|
|
1830
|
+
gpuRingSize + " slots in GPU ring buffer");
|
|
1831
|
+
} else {
|
|
1832
|
+
logD("VRAM mode: Loading texture array with ASTC=" + useAstc);
|
|
1833
|
+
|
|
1834
|
+
int[] result = AlphaVideoGpuNative.nativeLoadTextureArray(ktx2Data, useAstc);
|
|
1835
|
+
if (result == null || result.length < 4) {
|
|
1836
|
+
throw new Exception("Failed to load texture array");
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
textureArrayId = result[0];
|
|
1840
|
+
int texWidth = result[1];
|
|
1841
|
+
int texHeight = result[2];
|
|
1842
|
+
textureArrayLayerCount = result[3];
|
|
1843
|
+
ramHeavyMode = false;
|
|
1844
|
+
|
|
1845
|
+
logD("Loaded texture array ID " + textureArrayId +
|
|
1846
|
+
" (" + texWidth + "x" + texHeight + ", " + textureArrayLayerCount + " layers)");
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
private void renderFrame() {
|
|
1851
|
+
if (eglDisplay == null || eglSurface == null || textureArrayId == 0) {
|
|
1852
|
+
return;
|
|
1853
|
+
}
|
|
1854
|
+
renderTextureArrayFrame();
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
/**
|
|
1858
|
+
* Draws the current frame: samples its layer of the texture array.
|
|
1859
|
+
*/
|
|
1860
|
+
private void renderTextureArrayFrame() {
|
|
1861
|
+
if (arrayProgram == 0) {
|
|
1862
|
+
Log.e(TAG, "Texture array shader not initialized");
|
|
1863
|
+
return;
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
// RAM-heavy mode: Upload current frame to GPU ring buffer on-demand (zero-copy from direct ByteBuffer)
|
|
1867
|
+
if (ramHeavyMode && ramFrameBuffer != null && currentFrame < ramFrameCount) {
|
|
1868
|
+
if (ramUploadedFrame != currentFrame) {
|
|
1869
|
+
// Upload to slot 0 (single-buffered for simplicity)
|
|
1870
|
+
// Use zero-copy upload with offset into contiguous ByteBuffer
|
|
1871
|
+
int slot = 0;
|
|
1872
|
+
int offset = currentFrame * ramBytesPerFrame;
|
|
1873
|
+
boolean useAstc = astcSupported;
|
|
1874
|
+
boolean uploaded = AlphaVideoGpuNative.nativeUploadTextureArrayLayerFromBufferWithOffset(
|
|
1875
|
+
textureArrayId, slot, frameWidth, frameHeight, ramFrameBuffer, offset, ramBytesPerFrame, useAstc,
|
|
1876
|
+
astcGlFormat, astcBlockWidth, astcBlockHeight);
|
|
1877
|
+
if (uploaded) {
|
|
1878
|
+
ramUploadedFrame = currentFrame;
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
GLES30.glClearColor(0, 0, 0, 0);
|
|
1884
|
+
GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT);
|
|
1885
|
+
GLES30.glViewport(0, 0, getWidth(), getHeight());
|
|
1886
|
+
GLES30.glUseProgram(arrayProgram);
|
|
1887
|
+
|
|
1888
|
+
// Set layer uniform:
|
|
1889
|
+
// - RAM-heavy mode: Always slot 0 (we upload current frame there)
|
|
1890
|
+
// - Streaming ring buffer mode: currentFrame % ringSize
|
|
1891
|
+
// - VRAM mode (non-stream or VRAM-heavy stream): currentFrame directly
|
|
1892
|
+
int layerIndex;
|
|
1893
|
+
if (ramHeavyMode) {
|
|
1894
|
+
layerIndex = 0; // Always render from slot 0 where we just uploaded
|
|
1895
|
+
} else if (streamingRingBufferMode) {
|
|
1896
|
+
layerIndex = currentFrame % streamingRingSize;
|
|
1897
|
+
} else {
|
|
1898
|
+
layerIndex = currentFrame;
|
|
1899
|
+
}
|
|
1900
|
+
GLES30.glUniform1f(arrayUniformLayerLocation, (float) layerIndex);
|
|
1901
|
+
|
|
1902
|
+
// Bind texture array
|
|
1903
|
+
GLES30.glActiveTexture(GLES30.GL_TEXTURE0);
|
|
1904
|
+
GLES30.glBindTexture(GLES30.GL_TEXTURE_2D_ARRAY, textureArrayId);
|
|
1905
|
+
int textureLocation = GLES30.glGetUniformLocation(arrayProgram, "uTextureArray");
|
|
1906
|
+
GLES30.glUniform1i(textureLocation, 0);
|
|
1907
|
+
|
|
1908
|
+
// Set vertex attributes
|
|
1909
|
+
int positionHandle = GLES30.glGetAttribLocation(arrayProgram, "aPosition");
|
|
1910
|
+
GLES30.glEnableVertexAttribArray(positionHandle);
|
|
1911
|
+
GLES30.glVertexAttribPointer(positionHandle, 2, GLES30.GL_FLOAT, false, 0, vertexBuffer);
|
|
1912
|
+
|
|
1913
|
+
int texCoordHandle = GLES30.glGetAttribLocation(arrayProgram, "aTexCoord");
|
|
1914
|
+
GLES30.glEnableVertexAttribArray(texCoordHandle);
|
|
1915
|
+
GLES30.glVertexAttribPointer(texCoordHandle, 2, GLES30.GL_FLOAT, false, 0, texCoordBuffer);
|
|
1916
|
+
|
|
1917
|
+
// Draw
|
|
1918
|
+
GLES30.glDrawArrays(GLES30.GL_TRIANGLE_STRIP, 0, 4);
|
|
1919
|
+
|
|
1920
|
+
GLES30.glDisableVertexAttribArray(positionHandle);
|
|
1921
|
+
GLES30.glDisableVertexAttribArray(texCoordHandle);
|
|
1922
|
+
|
|
1923
|
+
EGL14.eglSwapBuffers(eglDisplay, eglSurface);
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
private void cleanupGL() {
|
|
1927
|
+
if (eglDisplay != null && eglDisplay != EGL14.EGL_NO_DISPLAY) {
|
|
1928
|
+
// The native side finds this view's PBOs by the current context
|
|
1929
|
+
if (pbosInitialized && textureArrayId != 0) {
|
|
1930
|
+
AlphaVideoGpuNative.nativeCleanupPBOs(textureArrayId);
|
|
1931
|
+
pbosInitialized = false;
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
EGL14.eglMakeCurrent(eglDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_CONTEXT);
|
|
1935
|
+
|
|
1936
|
+
// Destroying the context below frees the texture array and program
|
|
1937
|
+
textureArrayId = 0;
|
|
1938
|
+
arrayProgram = 0;
|
|
1939
|
+
|
|
1940
|
+
// Reset streaming ring buffer mode
|
|
1941
|
+
streamingRingBufferMode = false;
|
|
1942
|
+
streamingRingSize = 0;
|
|
1943
|
+
|
|
1944
|
+
// Clean up RAM-heavy mode resources
|
|
1945
|
+
ramFrameBuffer = null;
|
|
1946
|
+
ramBytesPerFrame = 0;
|
|
1947
|
+
ramFrameCount = 0;
|
|
1948
|
+
ramHeavyMode = false;
|
|
1949
|
+
ramUploadedFrame = -1;
|
|
1950
|
+
|
|
1951
|
+
if (eglSurface != null && eglSurface != EGL14.EGL_NO_SURFACE) {
|
|
1952
|
+
EGL14.eglDestroySurface(eglDisplay, eglSurface);
|
|
1953
|
+
eglSurface = null;
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
if (eglContext != null && eglContext != EGL14.EGL_NO_CONTEXT) {
|
|
1957
|
+
EGL14.eglDestroyContext(eglDisplay, eglContext);
|
|
1958
|
+
eglContext = null;
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
EGL14.eglTerminate(eglDisplay);
|
|
1962
|
+
eglDisplay = null;
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
private static boolean isRemote(String uri) {
|
|
1967
|
+
return uri.startsWith("https://") || uri.startsWith("http://");
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
/**
|
|
1971
|
+
* What a source URI reads from: a local file in place, or for a URL or a data source a range
|
|
1972
|
+
* reader, opened here. Blocks on the network.
|
|
1973
|
+
*/
|
|
1974
|
+
private AlphaVideoGpuSource resolveSource(String uri, String cacheKey) throws IOException {
|
|
1975
|
+
final String dataSourceName = mPlayingDataSource;
|
|
1976
|
+
final boolean appSource = dataSourceName != null;
|
|
1977
|
+
if (!appSource && !isRemote(uri)) return AlphaVideoGpuSource.parse(uri);
|
|
1978
|
+
if (!RangeSourceLink.available()) {
|
|
1979
|
+
if (appSource) {
|
|
1980
|
+
throw new NoSourceModuleException("The data source \"" + dataSourceName + "\" needs the "
|
|
1981
|
+
+ "range-source add-on (dev.mediacache:range-source; for React Native, react-native-range-source), "
|
|
1982
|
+
+ "which the app doesn't include");
|
|
1983
|
+
}
|
|
1984
|
+
warnWithoutAddOn();
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
final Fetcher source;
|
|
1988
|
+
if (appSource) {
|
|
1989
|
+
source = AppDataSource.create(dataSourceName);
|
|
1990
|
+
if (source == null) {
|
|
1991
|
+
throw new IOException("No data source is registered as \"" + dataSourceName
|
|
1992
|
+
+ "\" (RangeDataSources.register)");
|
|
1993
|
+
}
|
|
1994
|
+
} else {
|
|
1995
|
+
source = new HttpFetcher();
|
|
1996
|
+
}
|
|
1997
|
+
AlphaVideoGpuReader.Storage storage = storageFor(appSource);
|
|
1998
|
+
String storeDir = storage == AlphaVideoGpuReader.Storage.DISK
|
|
1999
|
+
? new File(getContext().getCacheDir(), STORE_NAME).getPath() : STORE_NAME;
|
|
2000
|
+
String key = cacheKey != null && !cacheKey.isEmpty() ? cacheKey : uri;
|
|
2001
|
+
AlphaVideoGpuReader reader = AlphaVideoGpuReader.open(source, Uri.parse(uri), mPlayingHeaders, storage,
|
|
2002
|
+
storeDir, key, mMaxStorageBytes, getContext().getCacheDir().getPath());
|
|
2003
|
+
logD("Opened " + uri + " (" + reader.size() + " bytes, storage " + storage + ")");
|
|
2004
|
+
return AlphaVideoGpuSource.reader(reader, shouldStop);
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
/** A data source without the range-source add-on: reported with {@link #ERROR_NO_SOURCE_MODULE}. */
|
|
2008
|
+
private static final class NoSourceModuleException extends IOException {
|
|
2009
|
+
NoSourceModuleException(String message) {
|
|
2010
|
+
super(message);
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
private void warnWithoutAddOn() {
|
|
2015
|
+
if (warnedWithoutAddOn.compareAndSet(false, true)) {
|
|
2016
|
+
Log.w(TAG, "range-source isn't installed (dev.mediacache:range-source; for React Native, "
|
|
2017
|
+
+ "npm i react-native-range-source): URLs are downloaded whole for each play, without range "
|
|
2018
|
+
+ "requests, storage, maxStorageBytes or cacheKey");
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
private AlphaVideoGpuReader.Storage storageFor(boolean appSource) {
|
|
2023
|
+
switch (mStorage) {
|
|
2024
|
+
case STORAGE_DISK: return AlphaVideoGpuReader.Storage.DISK;
|
|
2025
|
+
case STORAGE_MEMORY: return AlphaVideoGpuReader.Storage.MEMORY;
|
|
2026
|
+
case STORAGE_NONE: return AlphaVideoGpuReader.Storage.NONE;
|
|
2027
|
+
default: return appSource ? AlphaVideoGpuReader.Storage.NONE : AlphaVideoGpuReader.Storage.DISK;
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
/** What to report when a zstd stream fails: a read that failed after its retries, or corrupt data. */
|
|
2032
|
+
private static String streamFailure(AlphaVideoGpuSource local, long streamId) {
|
|
2033
|
+
String why = AlphaVideoGpuNative.nativeStreamingError(streamId);
|
|
2034
|
+
if (why == null) why = "unknown error";
|
|
2035
|
+
return local.reader() != null ? "Reading " + local + " failed: " + why : "Cannot decode " + local + ": " + why;
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
/**
|
|
2039
|
+
* Points the stream at a zstd frame of the level (0 is its start), opening it again if an
|
|
2040
|
+
* earlier open failed. False when the stream can't be used.
|
|
2041
|
+
*/
|
|
2042
|
+
private boolean restartInternalZstdStream(long[] streamId, AlphaVideoGpuSource local,
|
|
2043
|
+
long level0Offset, long level0Length, long compressedOffset) {
|
|
2044
|
+
if (streamId[0] == 0) {
|
|
2045
|
+
try {
|
|
2046
|
+
streamId[0] = openInternalZstdStream(local, level0Offset, level0Length);
|
|
2047
|
+
} catch (IOException e) {
|
|
2048
|
+
Log.e(TAG, "Cannot reopen " + local + ": " + e.getMessage());
|
|
2049
|
+
return false;
|
|
2050
|
+
}
|
|
2051
|
+
if (streamId[0] == 0) return false;
|
|
2052
|
+
}
|
|
2053
|
+
return AlphaVideoGpuNative.nativeStreamingRestartAt(streamId[0], compressedOffset);
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
/**
|
|
2057
|
+
* Starts a zstd stream over the level0 data of an internally compressed KTX2: memory-mapped in
|
|
2058
|
+
* place, or read from the source's range reader.
|
|
2059
|
+
*/
|
|
2060
|
+
private long openInternalZstdStream(AlphaVideoGpuSource local, long level0Offset, long level0Length) throws IOException {
|
|
2061
|
+
AlphaVideoGpuReader reader = local.reader();
|
|
2062
|
+
if (reader != null) {
|
|
2063
|
+
return AlphaVideoGpuNative.nativeReaderZstdStreamingInit(reader.handle(), level0Offset, level0Length);
|
|
2064
|
+
}
|
|
2065
|
+
try (AssetFileDescriptor afd = local.openFd(getContext())) {
|
|
2066
|
+
return AlphaVideoGpuNative.nativeInternalZstdStreamingInit(
|
|
2067
|
+
afd.getParcelFileDescriptor().getFd(), afd.getStartOffset() + level0Offset, level0Length);
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
private void logD(String message) {
|
|
2072
|
+
if (verbose) Log.d(TAG, message);
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
private void logI(String message) {
|
|
2076
|
+
if (verbose) Log.i(TAG, message);
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
// TextureView.SurfaceTextureListener implementation
|
|
2080
|
+
@Override
|
|
2081
|
+
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
|
|
2082
|
+
logD("onSurfaceTextureAvailable: " + width + "x" + height);
|
|
2083
|
+
isDestroyed.set(false);
|
|
2084
|
+
shouldStop.set(false);
|
|
2085
|
+
// Always start playback if we have a source - handles screen navigation
|
|
2086
|
+
if (mSource != null) {
|
|
2087
|
+
startPlayback();
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
@Override
|
|
2092
|
+
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
|
|
2093
|
+
logD("onSurfaceTextureSizeChanged: " + width + "x" + height);
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
@Override
|
|
2097
|
+
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
|
|
2098
|
+
logD("onSurfaceTextureDestroyed");
|
|
2099
|
+
isDestroyed.set(true);
|
|
2100
|
+
shouldStop.set(true);
|
|
2101
|
+
isPlaying.set(false);
|
|
2102
|
+
isPlaybackRunning.set(false); // Reset for clean restart
|
|
2103
|
+
mSourceLoaded = false; // Reset so playback restarts when surface is available again
|
|
2104
|
+
shutdownExecutor();
|
|
2105
|
+
return true;
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
@Override
|
|
2109
|
+
public void onSurfaceTextureUpdated(SurfaceTexture surface) {
|
|
2110
|
+
// No-op
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
@Override
|
|
2114
|
+
protected void onDetachedFromWindow() {
|
|
2115
|
+
super.onDetachedFromWindow();
|
|
2116
|
+
logD("onDetachedFromWindow");
|
|
2117
|
+
destroy();
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
@Override
|
|
2121
|
+
protected void onAttachedToWindow() {
|
|
2122
|
+
super.onAttachedToWindow();
|
|
2123
|
+
logD("onAttachedToWindow");
|
|
2124
|
+
isDestroyed.set(false);
|
|
2125
|
+
}
|
|
2126
|
+
}
|