react-native-alpha-video-gpu 0.0.0-stage → 1.0.1
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 +953 -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 +78 -4
- package/react-native/android/build.gradle +95 -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 +296 -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,37 @@
|
|
|
1
|
+
package dev.mediacache.alphavideo.gpu;
|
|
2
|
+
|
|
3
|
+
import android.net.Uri;
|
|
4
|
+
|
|
5
|
+
import java.io.IOException;
|
|
6
|
+
import java.util.Map;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Where {@link AlphaVideoGpuReader}'s bytes come from: the built-in {@link HttpFetcher}, or an app's
|
|
10
|
+
* data source through the range-source add-on ({@link AppDataSource}). The shape of range-source's
|
|
11
|
+
* {@code RangeDataSource}, so that the player reads URLs without the add-on too.
|
|
12
|
+
*/
|
|
13
|
+
interface Fetcher {
|
|
14
|
+
|
|
15
|
+
final class Info {
|
|
16
|
+
static final long UNKNOWN_SIZE = -1;
|
|
17
|
+
|
|
18
|
+
final long size;
|
|
19
|
+
final String etag;
|
|
20
|
+
final String lastModified;
|
|
21
|
+
|
|
22
|
+
Info(long size, String etag, String lastModified) {
|
|
23
|
+
this.size = size >= 0 ? size : UNKNOWN_SIZE;
|
|
24
|
+
this.etag = etag;
|
|
25
|
+
this.lastModified = lastModified;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
Info open(Uri uri, long position, Map<String, String> headers) throws IOException;
|
|
30
|
+
|
|
31
|
+
/** How many bytes were read, or -1 at the end of the file. */
|
|
32
|
+
int read(byte[] buffer, int offset, int length) throws IOException;
|
|
33
|
+
|
|
34
|
+
void close() throws IOException;
|
|
35
|
+
|
|
36
|
+
void cancel();
|
|
37
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
package dev.mediacache.alphavideo.gpu;
|
|
2
|
+
|
|
3
|
+
import android.net.Uri;
|
|
4
|
+
|
|
5
|
+
import java.io.IOException;
|
|
6
|
+
import java.io.InputStream;
|
|
7
|
+
import java.net.HttpURLConnection;
|
|
8
|
+
import java.net.URL;
|
|
9
|
+
import java.util.Map;
|
|
10
|
+
import java.util.regex.Matcher;
|
|
11
|
+
import java.util.regex.Pattern;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The player's fetcher for {@code http(s)://} URLs, with or without the range-source add-on:
|
|
15
|
+
* {@code HttpURLConnection} with {@code Range: bytes=N-} requests. A server that ignores ranges
|
|
16
|
+
* answers 200 with the whole file, which is read from the position on; that works, but each seek
|
|
17
|
+
* then reads from the start. Redirects are followed, between http and https too, keeping the
|
|
18
|
+
* headers.
|
|
19
|
+
*/
|
|
20
|
+
final class HttpFetcher implements Fetcher {
|
|
21
|
+
|
|
22
|
+
private static final int CONNECT_TIMEOUT_MS = 15000;
|
|
23
|
+
private static final int READ_TIMEOUT_MS = 30000;
|
|
24
|
+
private static final String USER_AGENT = "AlphaVideoGpu/1.0";
|
|
25
|
+
|
|
26
|
+
private static final int MAX_REDIRECTS = 5;
|
|
27
|
+
private static final Pattern CONTENT_RANGE = Pattern.compile("bytes\\s+(\\d+)-\\d+/(\\d+|\\*)");
|
|
28
|
+
private static final Pattern UNSATISFIED_RANGE = Pattern.compile("bytes\\s+\\*/(\\d+)");
|
|
29
|
+
|
|
30
|
+
// Set on the reading thread, disconnected by cancel() from any
|
|
31
|
+
private volatile HttpURLConnection connection;
|
|
32
|
+
private volatile boolean cancelled = false;
|
|
33
|
+
// Null once the read has ended
|
|
34
|
+
private InputStream in;
|
|
35
|
+
|
|
36
|
+
@Override
|
|
37
|
+
public Info open(Uri uri, long position, Map<String, String> headers) throws IOException {
|
|
38
|
+
close();
|
|
39
|
+
URL url = new URL(uri.toString());
|
|
40
|
+
for (int redirects = 0; ; redirects++) {
|
|
41
|
+
HttpURLConnection c = connect(url, position, headers);
|
|
42
|
+
int status = c.getResponseCode();
|
|
43
|
+
if (isRedirect(status)) {
|
|
44
|
+
String location = c.getHeaderField("Location");
|
|
45
|
+
close();
|
|
46
|
+
if (location == null) throw new IOException("HTTP " + status + " without a Location for " + url);
|
|
47
|
+
if (redirects == MAX_REDIRECTS) throw new IOException("Too many redirects for " + uri);
|
|
48
|
+
url = new URL(url, location);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
String etag = c.getHeaderField("ETag");
|
|
52
|
+
String lastModified = c.getHeaderField("Last-Modified");
|
|
53
|
+
switch (status) {
|
|
54
|
+
case HttpURLConnection.HTTP_PARTIAL: {
|
|
55
|
+
String contentRange = c.getHeaderField("Content-Range");
|
|
56
|
+
Matcher range = CONTENT_RANGE.matcher(String.valueOf(contentRange));
|
|
57
|
+
if (!range.lookingAt() || Long.parseLong(range.group(1)) != position) {
|
|
58
|
+
close();
|
|
59
|
+
throw new IOException("Asked " + url + " for bytes from " + position + " and got " + contentRange);
|
|
60
|
+
}
|
|
61
|
+
in = c.getInputStream();
|
|
62
|
+
String total = range.group(2);
|
|
63
|
+
return new Info(total.equals("*") ? Info.UNKNOWN_SIZE : Long.parseLong(total), etag, lastModified);
|
|
64
|
+
}
|
|
65
|
+
case HttpURLConnection.HTTP_OK: {
|
|
66
|
+
long size = c.getContentLengthLong();
|
|
67
|
+
in = c.getInputStream();
|
|
68
|
+
skip(position);
|
|
69
|
+
return new Info(size, etag, lastModified);
|
|
70
|
+
}
|
|
71
|
+
case 416: {
|
|
72
|
+
// At or past the end: a read that ends at once
|
|
73
|
+
Matcher range = UNSATISFIED_RANGE.matcher(String.valueOf(c.getHeaderField("Content-Range")));
|
|
74
|
+
long size = range.lookingAt() ? Long.parseLong(range.group(1)) : Info.UNKNOWN_SIZE;
|
|
75
|
+
close();
|
|
76
|
+
return new Info(size, etag, lastModified);
|
|
77
|
+
}
|
|
78
|
+
default:
|
|
79
|
+
close();
|
|
80
|
+
throw new IOException("HTTP " + status + " for " + url);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
@Override
|
|
86
|
+
public int read(byte[] buffer, int offset, int length) throws IOException {
|
|
87
|
+
if (in == null) return -1;
|
|
88
|
+
try {
|
|
89
|
+
return in.read(buffer, offset, length);
|
|
90
|
+
} catch (IOException e) {
|
|
91
|
+
if (cancelled) throw new IOException("Cancelled");
|
|
92
|
+
throw e;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
@Override
|
|
97
|
+
public void close() {
|
|
98
|
+
InputStream stream = in;
|
|
99
|
+
in = null;
|
|
100
|
+
if (stream != null) {
|
|
101
|
+
try {
|
|
102
|
+
stream.close();
|
|
103
|
+
} catch (IOException ignored) {
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
HttpURLConnection c = connection;
|
|
107
|
+
connection = null;
|
|
108
|
+
if (c != null) c.disconnect();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
@Override
|
|
112
|
+
public void cancel() {
|
|
113
|
+
cancelled = true;
|
|
114
|
+
HttpURLConnection c = connection;
|
|
115
|
+
if (c != null) c.disconnect();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private HttpURLConnection connect(URL url, long position, Map<String, String> headers) throws IOException {
|
|
119
|
+
String protocol = url.getProtocol();
|
|
120
|
+
if (!protocol.equals("https") && !protocol.equals("http")) {
|
|
121
|
+
throw new IOException("Not an http(s) URL: " + url);
|
|
122
|
+
}
|
|
123
|
+
HttpURLConnection c = (HttpURLConnection) url.openConnection();
|
|
124
|
+
connection = c;
|
|
125
|
+
if (cancelled) {
|
|
126
|
+
close();
|
|
127
|
+
throw new IOException("Cancelled");
|
|
128
|
+
}
|
|
129
|
+
c.setConnectTimeout(CONNECT_TIMEOUT_MS);
|
|
130
|
+
c.setReadTimeout(READ_TIMEOUT_MS);
|
|
131
|
+
c.setInstanceFollowRedirects(false);
|
|
132
|
+
c.setRequestProperty("User-Agent", USER_AGENT);
|
|
133
|
+
for (Map.Entry<String, String> header : headers.entrySet()) {
|
|
134
|
+
c.setRequestProperty(header.getKey(), header.getValue());
|
|
135
|
+
}
|
|
136
|
+
// Offsets are into the file as stored, so no transparent gzip
|
|
137
|
+
c.setRequestProperty("Accept-Encoding", "identity");
|
|
138
|
+
c.setRequestProperty("Range", "bytes=" + position + "-");
|
|
139
|
+
return c;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Reads past the first {@code count} bytes of a 200 answer; stops early at its end. */
|
|
143
|
+
private void skip(long count) throws IOException {
|
|
144
|
+
byte[] scratch = new byte[64 * 1024];
|
|
145
|
+
while (count > 0) {
|
|
146
|
+
int n = in.read(scratch, 0, (int) Math.min(scratch.length, count));
|
|
147
|
+
if (n < 0) return;
|
|
148
|
+
count -= n;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private static boolean isRedirect(int status) {
|
|
153
|
+
return status == HttpURLConnection.HTTP_MOVED_PERM || status == HttpURLConnection.HTTP_MOVED_TEMP
|
|
154
|
+
|| status == HttpURLConnection.HTTP_SEE_OTHER || status == 307 || status == 308;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
package dev.mediacache.alphavideo.gpu;
|
|
2
|
+
|
|
3
|
+
import java.nio.ByteBuffer;
|
|
4
|
+
import java.nio.ByteOrder;
|
|
5
|
+
import java.nio.charset.StandardCharsets;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A KTX2 file's frame metadata: size, frame count and vkFormat from the header, and the frame rate
|
|
9
|
+
* from KTXanimData, the KTX 2.0 key for array textures whose layers are animation frames (per-frame
|
|
10
|
+
* duration / timescale seconds, so 29.97 fps is exact).
|
|
11
|
+
*/
|
|
12
|
+
final class Ktx2Metadata {
|
|
13
|
+
/** Frame rate for files without KTXanimData. */
|
|
14
|
+
static final double DEFAULT_FRAME_RATE = 30.0;
|
|
15
|
+
|
|
16
|
+
private static final byte[] ANIM_DATA_KEY = "KTXanimData".getBytes(StandardCharsets.US_ASCII);
|
|
17
|
+
|
|
18
|
+
final int vkFormat;
|
|
19
|
+
final int width;
|
|
20
|
+
final int height;
|
|
21
|
+
final int frameCount;
|
|
22
|
+
final double frameRate;
|
|
23
|
+
|
|
24
|
+
private Ktx2Metadata(int vkFormat, int width, int height, int frameCount, double frameRate) {
|
|
25
|
+
this.vkFormat = vkFormat;
|
|
26
|
+
this.width = width;
|
|
27
|
+
this.height = height;
|
|
28
|
+
this.frameCount = frameCount;
|
|
29
|
+
this.frameRate = frameRate;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Reads data holding the file through its key/value data; null if it isn't KTX2. */
|
|
33
|
+
static Ktx2Metadata read(byte[] ktx2) {
|
|
34
|
+
if (ktx2 == null || ktx2.length < 80 || ktx2[0] != (byte) 0xAB || ktx2[1] != 0x4B
|
|
35
|
+
|| ktx2[2] != 0x54 || ktx2[3] != 0x58) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
ByteBuffer header = ByteBuffer.wrap(ktx2).order(ByteOrder.LITTLE_ENDIAN);
|
|
39
|
+
double animFrameRate = animFrameRate(ktx2, header.getInt(56), header.getInt(60));
|
|
40
|
+
return new Ktx2Metadata(header.getInt(12), header.getInt(20), header.getInt(24), header.getInt(32),
|
|
41
|
+
animFrameRate > 0 ? animFrameRate : DEFAULT_FRAME_RATE);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** ASTC (VkFormat 157-184, either colour space) in an array texture: the only files that play. */
|
|
45
|
+
boolean isAstcArray() {
|
|
46
|
+
return vkFormat >= 157 && vkFormat <= 184 && frameCount >= 1;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The frame rate from KTXanimData in the key/value data, or 0 if there's none. */
|
|
50
|
+
private static double animFrameRate(byte[] ktx2, int kvdOffset, int kvdLength) {
|
|
51
|
+
if (kvdOffset < 0 || kvdLength <= 0) return 0;
|
|
52
|
+
ByteBuffer data = ByteBuffer.wrap(ktx2).order(ByteOrder.LITTLE_ENDIAN);
|
|
53
|
+
long kvdEnd = Math.min((long) kvdOffset + kvdLength, ktx2.length);
|
|
54
|
+
long pos = kvdOffset;
|
|
55
|
+
while (pos + 4 <= kvdEnd) {
|
|
56
|
+
long entryLength = data.getInt((int) pos) & 0xFFFFFFFFL;
|
|
57
|
+
int entryStart = (int) pos + 4;
|
|
58
|
+
if (entryLength == 0 || entryStart + entryLength > kvdEnd) break;
|
|
59
|
+
int keyEnd = entryStart;
|
|
60
|
+
while (keyEnd < entryStart + entryLength && ktx2[keyEnd] != 0) keyEnd++;
|
|
61
|
+
if (isAnimDataKey(ktx2, entryStart, keyEnd) && entryStart + entryLength >= keyEnd + 1 + 12) {
|
|
62
|
+
long duration = data.getInt(keyEnd + 1) & 0xFFFFFFFFL;
|
|
63
|
+
long timescale = data.getInt(keyEnd + 5) & 0xFFFFFFFFL;
|
|
64
|
+
return duration > 0 && timescale > 0 ? (double) timescale / duration : 0;
|
|
65
|
+
}
|
|
66
|
+
pos = entryStart + entryLength + (4 - entryLength % 4) % 4;
|
|
67
|
+
}
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private static boolean isAnimDataKey(byte[] ktx2, int start, int end) {
|
|
72
|
+
if (end - start != ANIM_DATA_KEY.length) return false;
|
|
73
|
+
for (int i = 0; i < ANIM_DATA_KEY.length; i++) {
|
|
74
|
+
if (ktx2[start + i] != ANIM_DATA_KEY[i]) return false;
|
|
75
|
+
}
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
package dev.mediacache.alphavideo.gpu;
|
|
2
|
+
|
|
3
|
+
import android.util.Log;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The range-source add-on ({@code dev.mediacache:range-source}), if the app has it: its native
|
|
7
|
+
* table of functions, which {@link AlphaVideoGpuReader} reads URLs and data sources through. The player
|
|
8
|
+
* depends on it, so it's there unless the app's build excludes it. Without it, a URL is downloaded
|
|
9
|
+
* whole into a temporary file (the basic input), and data sources, storage and cache keys aren't
|
|
10
|
+
* available.
|
|
11
|
+
*/
|
|
12
|
+
final class RangeSourceLink {
|
|
13
|
+
private static final String TAG = "AlphaVideoGpu";
|
|
14
|
+
// Its RangeSource class has a static plugin(), the address of its RangeSourcePlugin
|
|
15
|
+
private static final String ADD_ON = "dev.mediacache.rangesource.RangeSource";
|
|
16
|
+
|
|
17
|
+
static {
|
|
18
|
+
System.loadLibrary("alphavideogpu");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
private static final long PLUGIN = discover();
|
|
22
|
+
|
|
23
|
+
private RangeSourceLink() {}
|
|
24
|
+
|
|
25
|
+
/** The add-on's table, or 0 without it. */
|
|
26
|
+
static long plugin() {
|
|
27
|
+
return PLUGIN;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static boolean available() {
|
|
31
|
+
return PLUGIN != 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private static long discover() {
|
|
35
|
+
Class<?> addOn;
|
|
36
|
+
try {
|
|
37
|
+
addOn = Class.forName(ADD_ON);
|
|
38
|
+
} catch (ClassNotFoundException e) {
|
|
39
|
+
return 0;
|
|
40
|
+
} catch (LinkageError e) {
|
|
41
|
+
Log.e(TAG, "range-source is there but its native library didn't load", e);
|
|
42
|
+
return 0;
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
return nativeUsablePlugin((Long) addOn.getMethod("plugin").invoke(null));
|
|
46
|
+
} catch (ReflectiveOperationException | RuntimeException | LinkageError e) {
|
|
47
|
+
Log.e(TAG, "range-source didn't give its table", e);
|
|
48
|
+
return 0;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** {@code plugin} if this player can use it (its ABI and functions), else 0, logging why. */
|
|
53
|
+
private static native long nativeUsablePlugin(long plugin);
|
|
54
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
package dev.mediacache.alphavideo.gpu;
|
|
2
|
+
|
|
3
|
+
import androidx.annotation.Nullable;
|
|
4
|
+
|
|
5
|
+
import java.nio.ByteBuffer;
|
|
6
|
+
import java.nio.ByteOrder;
|
|
7
|
+
import java.util.Arrays;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The seek table texenc writes after a KTX2 level's zstd frames: zstd's seekable format
|
|
11
|
+
* (contrib/seekable_format), a skippable frame listing each frame's compressed and decompressed
|
|
12
|
+
* size, ending with a 9-byte footer. The decoder can start at any frame. A level without one is
|
|
13
|
+
* a single stream, which decodes only from its start.
|
|
14
|
+
*
|
|
15
|
+
* It sits at the end of the level, so the player reads the level's last few KB for it, not the
|
|
16
|
+
* level: {@link #tailBytesNeeded}, then {@link #parseTail}.
|
|
17
|
+
*/
|
|
18
|
+
final class ZstdSeekTable {
|
|
19
|
+
private static final int SEEK_TABLE_MAGIC = 0x184D2A5E;
|
|
20
|
+
private static final int SEEKABLE_FOOTER_MAGIC = 0x8F92EAB1;
|
|
21
|
+
private static final int FOOTER_SIZE = 9;
|
|
22
|
+
private static final int SKIPPABLE_HEADER_SIZE = 8;
|
|
23
|
+
private static final int CHECKSUM_FLAG = 0x80;
|
|
24
|
+
private static final int RESERVED_BITS = 0x7C;
|
|
25
|
+
|
|
26
|
+
/** Bytes of the level's tail to read first; enough for the table of any clip under 8000 seek points. */
|
|
27
|
+
static final int TAIL_GUESS = 64 * 1024;
|
|
28
|
+
|
|
29
|
+
/** Where each frame starts in the level data, ascending from 0. */
|
|
30
|
+
final long[] compressedOffsets;
|
|
31
|
+
/** Where each frame's output starts in the decompressed level, ascending from 0. */
|
|
32
|
+
final long[] decompressedOffsets;
|
|
33
|
+
|
|
34
|
+
private ZstdSeekTable(long[] compressedOffsets, long[] decompressedOffsets) {
|
|
35
|
+
this.compressedOffsets = compressedOffsets;
|
|
36
|
+
this.decompressedOffsets = decompressedOffsets;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* How many bytes from the level's end the table takes, footer included, read from the last
|
|
41
|
+
* bytes of the level ({@code tail}, at least the 9-byte footer); -1 when there is no table.
|
|
42
|
+
*/
|
|
43
|
+
static long tailBytesNeeded(byte[] tail) {
|
|
44
|
+
if (tail.length < FOOTER_SIZE) return -1;
|
|
45
|
+
ByteBuffer data = ByteBuffer.wrap(tail).order(ByteOrder.LITTLE_ENDIAN);
|
|
46
|
+
int end = tail.length;
|
|
47
|
+
if (data.getInt(end - 4) != SEEKABLE_FOOTER_MAGIC) return -1;
|
|
48
|
+
int descriptor = tail[end - 5] & 0xFF;
|
|
49
|
+
if ((descriptor & RESERVED_BITS) != 0) return -1;
|
|
50
|
+
long frameCount = data.getInt(end - FOOTER_SIZE) & 0xFFFFFFFFL;
|
|
51
|
+
int entrySize = (descriptor & CHECKSUM_FLAG) != 0 ? 12 : 8;
|
|
52
|
+
return SKIPPABLE_HEADER_SIZE + frameCount * entrySize + FOOTER_SIZE;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The table in the last bytes of a level ({@code tail}, ending where the level ends), or null
|
|
57
|
+
* when there is none, it doesn't fit in {@code tail}, or it doesn't describe exactly the
|
|
58
|
+
* frames before it.
|
|
59
|
+
*/
|
|
60
|
+
@Nullable
|
|
61
|
+
static ZstdSeekTable parseTail(byte[] tail, long levelLength, long decompressedLength) {
|
|
62
|
+
if (levelLength < SKIPPABLE_HEADER_SIZE + FOOTER_SIZE || tail.length > levelLength) return null;
|
|
63
|
+
long needed = tailBytesNeeded(tail);
|
|
64
|
+
if (needed < 0 || needed > tail.length) return null;
|
|
65
|
+
ByteBuffer data = ByteBuffer.wrap(tail).order(ByteOrder.LITTLE_ENDIAN);
|
|
66
|
+
int end = tail.length;
|
|
67
|
+
int descriptor = tail[end - 5] & 0xFF;
|
|
68
|
+
long frameCount = data.getInt(end - FOOTER_SIZE) & 0xFFFFFFFFL;
|
|
69
|
+
int entrySize = (descriptor & CHECKSUM_FLAG) != 0 ? 12 : 8;
|
|
70
|
+
|
|
71
|
+
long tableSize = needed - SKIPPABLE_HEADER_SIZE;
|
|
72
|
+
int tableStart = (int) (end - needed);
|
|
73
|
+
if (frameCount == 0) return null;
|
|
74
|
+
if (data.getInt(tableStart) != SEEK_TABLE_MAGIC) return null;
|
|
75
|
+
if ((data.getInt(tableStart + 4) & 0xFFFFFFFFL) != tableSize) return null;
|
|
76
|
+
|
|
77
|
+
long[] compressedOffsets = new long[(int) frameCount];
|
|
78
|
+
long[] decompressedOffsets = new long[(int) frameCount];
|
|
79
|
+
long compressed = 0;
|
|
80
|
+
long decompressed = 0;
|
|
81
|
+
for (int i = 0; i < frameCount; i++) {
|
|
82
|
+
int entry = tableStart + SKIPPABLE_HEADER_SIZE + i * entrySize;
|
|
83
|
+
compressedOffsets[i] = compressed;
|
|
84
|
+
decompressedOffsets[i] = decompressed;
|
|
85
|
+
compressed += data.getInt(entry) & 0xFFFFFFFFL;
|
|
86
|
+
decompressed += data.getInt(entry + 4) & 0xFFFFFFFFL;
|
|
87
|
+
}
|
|
88
|
+
if (compressed != levelLength - needed || decompressed != decompressedLength) return null;
|
|
89
|
+
return new ZstdSeekTable(compressedOffsets, decompressedOffsets);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** The table at the end of the level [levelOffset, levelOffset + levelLength) of a whole file. */
|
|
93
|
+
@Nullable
|
|
94
|
+
static ZstdSeekTable parse(byte[] file, long levelOffset, long levelLength, long decompressedLength) {
|
|
95
|
+
if (levelOffset < 0 || levelLength < 0 || levelOffset + levelLength > file.length) return null;
|
|
96
|
+
byte[] level = Arrays.copyOfRange(file, (int) levelOffset, (int) (levelOffset + levelLength));
|
|
97
|
+
return parseTail(level, levelLength, decompressedLength);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Where to start decoding to reach that decompressed offset: {compressed offset, decompressed
|
|
102
|
+
* offset} of the last frame starting at or before it, or the level start without a table.
|
|
103
|
+
*/
|
|
104
|
+
static long[] seekPointBefore(@Nullable ZstdSeekTable table, long decompressedOffset) {
|
|
105
|
+
if (table == null) return new long[] {0, 0};
|
|
106
|
+
long[] starts = table.decompressedOffsets;
|
|
107
|
+
int low = 0;
|
|
108
|
+
int high = starts.length - 1;
|
|
109
|
+
while (low < high) {
|
|
110
|
+
int mid = (low + high + 1) >>> 1;
|
|
111
|
+
if (starts[mid] <= decompressedOffset) low = mid;
|
|
112
|
+
else high = mid - 1;
|
|
113
|
+
}
|
|
114
|
+
return new long[] {table.compressedOffsets[low], starts[low]};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
int size() {
|
|
118
|
+
return compressedOffsets.length;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
package dev.mediacache.alphavideo.gpu;
|
|
2
|
+
|
|
3
|
+
import android.net.Uri;
|
|
4
|
+
|
|
5
|
+
import java.io.IOException;
|
|
6
|
+
import java.util.Map;
|
|
7
|
+
|
|
8
|
+
import dev.mediacache.rangesource.RangeDataSource;
|
|
9
|
+
import dev.mediacache.rangesource.RangeDataSources;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* An app's data source, registered with range-source's {@code RangeDataSources}, as a
|
|
13
|
+
* {@link Fetcher}. The only class that touches the add-on's types: load it only once
|
|
14
|
+
* {@link RangeSourceLink#available()}, as an app may leave the add-on out.
|
|
15
|
+
*/
|
|
16
|
+
final class AppDataSource implements Fetcher {
|
|
17
|
+
|
|
18
|
+
private final RangeDataSource source;
|
|
19
|
+
|
|
20
|
+
private AppDataSource(RangeDataSource source) {
|
|
21
|
+
this.source = source;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** A new source from the factory registered under {@code name}, or null if there's none. */
|
|
25
|
+
static Fetcher create(String name) {
|
|
26
|
+
RangeDataSource.Factory factory = RangeDataSources.get(name);
|
|
27
|
+
return factory != null ? new AppDataSource(factory.create()) : null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@Override
|
|
31
|
+
public Info open(Uri uri, long position, Map<String, String> headers) throws IOException {
|
|
32
|
+
RangeDataSource.Info info = source.open(uri, position, headers);
|
|
33
|
+
return new Info(info.size, info.etag, info.lastModified);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@Override
|
|
37
|
+
public int read(byte[] buffer, int offset, int length) throws IOException {
|
|
38
|
+
return source.read(buffer, offset, length);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@Override
|
|
42
|
+
public void close() throws IOException {
|
|
43
|
+
source.close();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@Override
|
|
47
|
+
public void cancel() {
|
|
48
|
+
source.cancel();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AlphaVideoGpuSource.h
|
|
3
|
+
* The library's range reader for URLs and data sources; not part of the public API.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
#import "AlphaVideoGPU.h"
|
|
7
|
+
|
|
8
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Whether the range-source add-on is there. Without it a URL is downloaded whole into a temporary
|
|
12
|
+
* file, and data sources, storage and cache keys aren't available.
|
|
13
|
+
*/
|
|
14
|
+
BOOL AVGRangeSourceAvailable(void);
|
|
15
|
+
|
|
16
|
+
/** What tryReadAt:into:length: returns while the bytes are on their way. */
|
|
17
|
+
static const int64_t AVGSourceReaderPending = -2;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A reader over a URL or a data source: with range-source, blocks in memory and in an optional
|
|
21
|
+
* store, fetched on the reader's own thread; without it, the URL downloaded into a temporary file,
|
|
22
|
+
* read as it arrives. Thread-safe.
|
|
23
|
+
*/
|
|
24
|
+
@interface AVGSourceReader : NSObject
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Opens the source at 0 to learn its size and version, unless the store holds it whole. Blocks
|
|
28
|
+
* on the network, so not on the main queue.
|
|
29
|
+
*
|
|
30
|
+
* @param dataSource the name of a data source registered with the add-on's RangeDataSources; nil
|
|
31
|
+
* for the built-in URLSession fetcher (http and https only)
|
|
32
|
+
* @param storage "disk" (Caches/alpha-video-gpu), "memory" or "none"; with the add-on
|
|
33
|
+
* @param key what the store keeps the file under; with the add-on
|
|
34
|
+
*/
|
|
35
|
+
+ (nullable instancetype)openURL:(NSURL *)url
|
|
36
|
+
dataSource:(nullable NSString *)dataSource
|
|
37
|
+
headers:(NSDictionary<NSString *, NSString *> *)headers
|
|
38
|
+
storage:(NSString *)storage
|
|
39
|
+
key:(NSString *)key
|
|
40
|
+
maxStorageBytes:(int64_t)maxStorageBytes
|
|
41
|
+
error:(NSString *_Nullable *_Nullable)error;
|
|
42
|
+
|
|
43
|
+
/** The file's size in bytes, or -1 while it isn't known. */
|
|
44
|
+
@property (nonatomic, readonly) int64_t size;
|
|
45
|
+
/** Why the last read failed. */
|
|
46
|
+
@property (nonatomic, readonly) NSString *lastError;
|
|
47
|
+
|
|
48
|
+
/** Reads exactly `length` bytes at `position`, waiting for them. Nil with `error` set if it can't, or once closed. */
|
|
49
|
+
- (nullable NSData *)readAt:(int64_t)position length:(int64_t)length error:(NSString *_Nullable *_Nullable)error;
|
|
50
|
+
/** The whole file, waiting for it. */
|
|
51
|
+
- (nullable NSData *)readAll:(NSString *_Nullable *_Nullable)error;
|
|
52
|
+
/**
|
|
53
|
+
* Copies up to `length` bytes at `position` without waiting: how many, 0 at the end of the file,
|
|
54
|
+
* AVGSourceReaderPending while they're on their way (they're asked for), or -1 once if fetching
|
|
55
|
+
* them failed (lastError says why).
|
|
56
|
+
*/
|
|
57
|
+
- (int64_t)tryReadAt:(int64_t)position into:(uint8_t *)buffer length:(size_t)length;
|
|
58
|
+
/** Stops the reader: reads waiting on it, and later ones, fail. */
|
|
59
|
+
- (void)close;
|
|
60
|
+
|
|
61
|
+
@end
|
|
62
|
+
|
|
63
|
+
NS_ASSUME_NONNULL_END
|