diff --git a/README.md b/README.md
index 320e5902..4595438f 100644
--- a/README.md
+++ b/README.md
@@ -116,6 +116,38 @@ var styles = StyleSheet.create({
});
```
+
+## Android Expansion File Usage
+
+```javascript
+// Within your render function, assuming you have a file called
+// "background.mp4" in your expansion file. Just add your main and (if applicable) patch version
+
+
+// Later on in your styles..
+var styles = Stylesheet.create({
+ backgroundVideo: {
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ bottom: 0,
+ right: 0,
+ },
+});
+```
+
### Load files with the RN Asset System
The asset system [introduced in RN `0.14`](http://www.reactnative.com/react-native-v0-14-0-released/) allows loading image resources shared across iOS and Android without touching native code. As of RN `0.31` [the same is true](https://github.com/facebook/react-native/commit/91ff6868a554c4930fd5fda6ba8044dbd56c8374) of mp4 video assets for Android. As of [RN `0.33`](https://github.com/facebook/react-native/releases/tag/v0.33.0) iOS is also supported. Requires `react-native-video@0.9.0`.
diff --git a/Video.js b/Video.js
index 53626d87..83d6599f 100644
--- a/Video.js
+++ b/Video.js
@@ -147,6 +147,8 @@ export default class Video extends Component {
isNetwork,
isAsset,
type: source.type || 'mp4',
+ mainVer: source.mainVer || 0,
+ patchVer: source.patchVer || 0,
},
onVideoLoadStart: this._onLoadStart,
onVideoLoad: this._onLoad,
diff --git a/android/src/main/java/com/android/vending/expansion/zipfile/APEZProvider.java b/android/src/main/java/com/android/vending/expansion/zipfile/APEZProvider.java
new file mode 100644
index 00000000..1ba910bf
--- /dev/null
+++ b/android/src/main/java/com/android/vending/expansion/zipfile/APEZProvider.java
@@ -0,0 +1,287 @@
+package com.android.vending.expansion.zipfile;
+
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+//To implement APEZProvider in your application, you'll want to change
+//the AUTHORITY to match what you define in the manifest.
+
+import com.android.vending.expansion.zipfile.ZipResourceFile.ZipEntryRO;
+
+import android.content.ContentProvider;
+import android.content.ContentProviderOperation;
+import android.content.ContentProviderResult;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.OperationApplicationException;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ProviderInfo;
+import android.content.res.AssetFileDescriptor;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.os.ParcelFileDescriptor;
+import android.provider.BaseColumns;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.ArrayList;
+
+/**
+ * This content provider is an optional part of the library.
+ *
+ *
Most apps don't need to use this class. This defines a
+ * ContentProvider that marshalls the data from the ZIP files through a
+ * content provider Uri in order to provide file access for certain Android APIs
+ * that expect Uri access to media files.
+ *
+ */
+public abstract class APEZProvider extends ContentProvider {
+
+ private ZipResourceFile mAPKExtensionFile;
+ private boolean mInit;
+
+ public static final String FILEID = BaseColumns._ID;
+ public static final String FILENAME = "ZPFN";
+ public static final String ZIPFILE = "ZFIL";
+ public static final String MODIFICATION = "ZMOD";
+ public static final String CRC32 = "ZCRC";
+ public static final String COMPRESSEDLEN = "ZCOL";
+ public static final String UNCOMPRESSEDLEN = "ZUNL";
+ public static final String COMPRESSIONTYPE = "ZTYP";
+
+ public static final String[] ALL_FIELDS = {
+ FILEID,
+ FILENAME,
+ ZIPFILE,
+ MODIFICATION,
+ CRC32,
+ COMPRESSEDLEN,
+ UNCOMPRESSEDLEN,
+ COMPRESSIONTYPE
+ };
+
+ public static final int FILEID_IDX = 0;
+ public static final int FILENAME_IDX = 1;
+ public static final int ZIPFILE_IDX = 2;
+ public static final int MOD_IDX = 3;
+ public static final int CRC_IDX = 4;
+ public static final int COMPLEN_IDX = 5;
+ public static final int UNCOMPLEN_IDX = 6;
+ public static final int COMPTYPE_IDX = 7;
+
+ public static final int[] ALL_FIELDS_INT = {
+ FILEID_IDX,
+ FILENAME_IDX,
+ ZIPFILE_IDX,
+ MOD_IDX,
+ CRC_IDX,
+ COMPLEN_IDX,
+ UNCOMPLEN_IDX,
+ COMPTYPE_IDX
+ };
+
+ /**
+ * This needs to match the authority in your manifest
+ */
+ public abstract String getAuthority();
+
+ @Override
+ public int delete(Uri arg0, String arg1, String[] arg2) {
+ // TODO Auto-generated method stub
+ return 0;
+ }
+
+ @Override
+ public String getType(Uri uri) {
+ return "vnd.android.cursor.item/asset";
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ static private final String NO_FILE = "N";
+
+ private boolean initIfNecessary() {
+ if ( !mInit ) {
+ Context ctx = getContext();
+ PackageManager pm = ctx.getPackageManager();
+ ProviderInfo pi = pm.resolveContentProvider(getAuthority(), PackageManager.GET_META_DATA);
+ PackageInfo packInfo;
+ try {
+ packInfo = pm.getPackageInfo(ctx.getPackageName(), 0);
+ } catch (NameNotFoundException e1) {
+ e1.printStackTrace();
+ return false;
+ }
+ int patchFileVersion;
+ int mainFileVersion;
+ int appVersionCode = packInfo.versionCode;
+ String[] resourceFiles = null;
+ if ( null != pi.metaData ) {
+ mainFileVersion = pi.metaData.getInt("mainVersion", appVersionCode);
+ patchFileVersion = pi.metaData.getInt("patchVersion", appVersionCode);
+ String mainFileName = pi.metaData.getString("mainFilename", NO_FILE);
+ if ( NO_FILE != mainFileName ) {
+ String patchFileName = pi.metaData.getString("patchFilename", NO_FILE);
+ if ( NO_FILE != patchFileName ) {
+ resourceFiles = new String[] { mainFileName, patchFileName };
+ } else {
+ resourceFiles = new String[] { mainFileName };
+ }
+ }
+ } else {
+ mainFileVersion = patchFileVersion = appVersionCode;
+ }
+ try {
+ if ( null == resourceFiles ) {
+ mAPKExtensionFile = APKExpansionSupport.getAPKExpansionZipFile(ctx, mainFileVersion, patchFileVersion);
+ } else {
+ mAPKExtensionFile = APKExpansionSupport.getResourceZipFile(resourceFiles);
+ }
+ mInit = true;
+ return true;
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onCreate() {
+ return true;
+ }
+
+ @Override
+ public AssetFileDescriptor openAssetFile(Uri uri, String mode)
+ throws FileNotFoundException {
+ initIfNecessary();
+ String path = uri.getEncodedPath();
+ if ( path.startsWith("/") ) {
+ path = path.substring(1);
+ }
+ return mAPKExtensionFile.getAssetFileDescriptor(path);
+ }
+
+ @Override
+ public ContentProviderResult[] applyBatch(
+ ArrayList operations)
+ throws OperationApplicationException {
+ initIfNecessary();
+ return super.applyBatch(operations);
+ }
+
+ @Override
+ public ParcelFileDescriptor openFile(Uri uri, String mode)
+ throws FileNotFoundException {
+ initIfNecessary();
+ AssetFileDescriptor af = openAssetFile(uri, mode);
+ if ( null != af ) {
+ return af.getParcelFileDescriptor();
+ }
+ return null;
+ }
+
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection,
+ String[] selectionArgs, String sortOrder) {
+ initIfNecessary();
+ // lists all of the items in the file that match
+ ZipEntryRO[] zipEntries;
+ if ( null == mAPKExtensionFile ) {
+ zipEntries = new ZipEntryRO[0];
+ } else {
+ zipEntries = mAPKExtensionFile.getAllEntries();
+ }
+ int[] intProjection;
+ if ( null == projection ) {
+ intProjection = ALL_FIELDS_INT;
+ projection = ALL_FIELDS;
+ } else {
+ int len = projection.length;
+ intProjection = new int[len];
+ for ( int i = 0; i < len; i++ ) {
+ if ( projection[i].equals(FILEID) ) {
+ intProjection[i] = FILEID_IDX;
+ } else if ( projection[i].equals(FILENAME) ) {
+ intProjection[i] = FILENAME_IDX;
+ } else if ( projection[i].equals(ZIPFILE) ) {
+ intProjection[i] = ZIPFILE_IDX;
+ } else if ( projection[i].equals(MODIFICATION) ) {
+ intProjection[i] = MOD_IDX;
+ } else if ( projection[i].equals(CRC32) ) {
+ intProjection[i] = CRC_IDX;
+ } else if ( projection[i].equals(COMPRESSEDLEN) ) {
+ intProjection[i] = COMPLEN_IDX;
+ } else if ( projection[i].equals(UNCOMPRESSEDLEN) ) {
+ intProjection[i] = UNCOMPLEN_IDX;
+ } else if ( projection[i].equals(COMPRESSIONTYPE) ) {
+ intProjection[i] = COMPTYPE_IDX;
+ } else {
+ throw new RuntimeException();
+ }
+ }
+ }
+ MatrixCursor mc = new MatrixCursor(projection, zipEntries.length);
+ int len = intProjection.length;
+ for ( ZipEntryRO zer : zipEntries ) {
+ MatrixCursor.RowBuilder rb = mc.newRow();
+ for ( int i = 0; i < len; i++ ) {
+ switch (intProjection[i]) {
+ case FILEID_IDX:
+ rb.add(i);
+ break;
+ case FILENAME_IDX:
+ rb.add(zer.mFileName);
+ break;
+ case ZIPFILE_IDX:
+ rb.add(zer.getZipFileName());
+ break;
+ case MOD_IDX:
+ rb.add(zer.mWhenModified);
+ break;
+ case CRC_IDX:
+ rb.add(zer.mCRC32);
+ break;
+ case COMPLEN_IDX:
+ rb.add(zer.mCompressedLength);
+ break;
+ case UNCOMPLEN_IDX:
+ rb.add(zer.mUncompressedLength);
+ break;
+ case COMPTYPE_IDX:
+ rb.add(zer.mMethod);
+ break;
+ }
+ }
+ }
+ return mc;
+ }
+
+ @Override
+ public int update(Uri uri, ContentValues values, String selection,
+ String[] selectionArgs) {
+ // TODO Auto-generated method stub
+ return 0;
+ }
+
+}
diff --git a/android/src/main/java/com/android/vending/expansion/zipfile/APKExpansionSupport.java b/android/src/main/java/com/android/vending/expansion/zipfile/APKExpansionSupport.java
new file mode 100644
index 00000000..34669c1d
--- /dev/null
+++ b/android/src/main/java/com/android/vending/expansion/zipfile/APKExpansionSupport.java
@@ -0,0 +1,81 @@
+package com.android.vending.expansion.zipfile;
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import java.io.File;
+import java.io.FileFilter;
+import java.io.IOException;
+import java.util.Vector;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import android.content.Context;
+import android.os.Environment;
+import android.util.Log;
+
+public class APKExpansionSupport {
+ // The shared path to all app expansion files
+ private final static String EXP_PATH = "/Android/obb/";
+
+ static String[] getAPKExpansionFiles(Context ctx, int mainVersion, int patchVersion) {
+ String packageName = ctx.getPackageName();
+ Vector ret = new Vector();
+ if (Environment.getExternalStorageState().equals(
+ Environment.MEDIA_MOUNTED)) {
+ // Build the full path to the app's expansion files
+ File root = Environment.getExternalStorageDirectory();
+ File expPath = new File(root.toString() + EXP_PATH + packageName);
+
+ // Check that expansion file path exists
+ if (expPath.exists()) {
+ if ( mainVersion > 0 ) {
+ String strMainPath = expPath + File.separator + "main." + mainVersion + "." + packageName + ".obb";
+// Log.d("APKEXPANSION", strMainPath);
+ File main = new File(strMainPath);
+ if ( main.isFile() ) {
+ ret.add(strMainPath);
+ }
+ }
+ if ( patchVersion > 0 ) {
+ String strPatchPath = expPath + File.separator + "patch." + patchVersion + "." + packageName + ".obb";
+ File main = new File(strPatchPath);
+ if ( main.isFile() ) {
+ ret.add(strPatchPath);
+ }
+ }
+ }
+ }
+ String[] retArray = new String[ret.size()];
+ ret.toArray(retArray);
+ return retArray;
+ }
+
+ static public ZipResourceFile getResourceZipFile(String[] expansionFiles) throws IOException {
+ ZipResourceFile apkExpansionFile = null;
+ for (String expansionFilePath : expansionFiles) {
+ if ( null == apkExpansionFile ) {
+ apkExpansionFile = new ZipResourceFile(expansionFilePath);
+ } else {
+ apkExpansionFile.addPatchFile(expansionFilePath);
+ }
+ }
+ return apkExpansionFile;
+ }
+
+ static public ZipResourceFile getAPKExpansionZipFile(Context ctx, int mainVersion, int patchVersion) throws IOException{
+ String[] expansionFiles = getAPKExpansionFiles(ctx, mainVersion, patchVersion);
+ return getResourceZipFile(expansionFiles);
+ }
+}
diff --git a/android/src/main/java/com/android/vending/expansion/zipfile/ZipResourceFile.java b/android/src/main/java/com/android/vending/expansion/zipfile/ZipResourceFile.java
new file mode 100644
index 00000000..902af3c4
--- /dev/null
+++ b/android/src/main/java/com/android/vending/expansion/zipfile/ZipResourceFile.java
@@ -0,0 +1,428 @@
+
+package com.android.vending.expansion.zipfile;
+
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import android.content.res.AssetFileDescriptor;
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+
+import java.io.EOFException;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.RandomAccessFile;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.MappedByteBuffer;
+import java.nio.channels.FileChannel;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Vector;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+public class ZipResourceFile {
+
+ //
+ // Read-only access to Zip archives, with minimal heap allocation.
+ //
+ static final String LOG_TAG = "zipro";
+ static final boolean LOGV = false;
+
+ // 4-byte number
+ static private int swapEndian(int i)
+ {
+ return ((i & 0xff) << 24) + ((i & 0xff00) << 8) + ((i & 0xff0000) >>> 8)
+ + ((i >>> 24) & 0xff);
+ }
+
+ // 2-byte number
+ static private int swapEndian(short i)
+ {
+ return ((i & 0x00FF) << 8 | (i & 0xFF00) >>> 8);
+ }
+
+ /*
+ * Zip file constants.
+ */
+ static final int kEOCDSignature = 0x06054b50;
+ static final int kEOCDLen = 22;
+ static final int kEOCDNumEntries = 8; // offset to #of entries in file
+ static final int kEOCDSize = 12; // size of the central directory
+ static final int kEOCDFileOffset = 16; // offset to central directory
+
+ static final int kMaxCommentLen = 65535; // longest possible in ushort
+ static final int kMaxEOCDSearch = (kMaxCommentLen + kEOCDLen);
+
+ static final int kLFHSignature = 0x04034b50;
+ static final int kLFHLen = 30; // excluding variable-len fields
+ static final int kLFHNameLen = 26; // offset to filename length
+ static final int kLFHExtraLen = 28; // offset to extra length
+
+ static final int kCDESignature = 0x02014b50;
+ static final int kCDELen = 46; // excluding variable-len fields
+ static final int kCDEMethod = 10; // offset to compression method
+ static final int kCDEModWhen = 12; // offset to modification timestamp
+ static final int kCDECRC = 16; // offset to entry CRC
+ static final int kCDECompLen = 20; // offset to compressed length
+ static final int kCDEUncompLen = 24; // offset to uncompressed length
+ static final int kCDENameLen = 28; // offset to filename length
+ static final int kCDEExtraLen = 30; // offset to extra length
+ static final int kCDECommentLen = 32; // offset to comment length
+ static final int kCDELocalOffset = 42; // offset to local hdr
+
+ static final int kCompressStored = 0; // no compression
+ static final int kCompressDeflated = 8; // standard deflate
+
+ /*
+ * The values we return for ZipEntryRO use 0 as an invalid value, so we want
+ * to adjust the hash table index by a fixed amount. Using a large value
+ * helps insure that people don't mix & match arguments, e.g. to
+ * findEntryByIndex().
+ */
+ static final int kZipEntryAdj = 10000;
+
+ static public final class ZipEntryRO {
+ public ZipEntryRO(final String zipFileName, final File file, final String fileName) {
+ mFileName = fileName;
+ mZipFileName = zipFileName;
+ mFile = file;
+ }
+
+ public final File mFile;
+ public final String mFileName;
+ public final String mZipFileName;
+ public long mLocalHdrOffset; // offset of local file header
+
+ /* useful stuff from the directory entry */
+ public int mMethod;
+ public long mWhenModified;
+ public long mCRC32;
+ public long mCompressedLength;
+ public long mUncompressedLength;
+
+ public long mOffset = -1;
+
+ public void setOffsetFromFile(RandomAccessFile f, ByteBuffer buf) throws IOException {
+ long localHdrOffset = mLocalHdrOffset;
+ try {
+ f.seek(localHdrOffset);
+ f.readFully(buf.array());
+ if (buf.getInt(0) != kLFHSignature) {
+ Log.w(LOG_TAG, "didn't find signature at start of lfh");
+ throw new IOException();
+ }
+ int nameLen = buf.getShort(kLFHNameLen) & 0xFFFF;
+ int extraLen = buf.getShort(kLFHExtraLen) & 0xFFFF;
+ mOffset = localHdrOffset + kLFHLen + nameLen + extraLen;
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ } catch (IOException ioe) {
+ ioe.printStackTrace();
+ }
+ }
+
+ /**
+ * Calculates the offset of the start of the Zip file entry within the
+ * Zip file.
+ *
+ * @return the offset, in bytes from the start of the file of the entry
+ */
+ public long getOffset() {
+ return mOffset;
+ }
+
+ /**
+ * isUncompressed
+ *
+ * @return true if the file is stored in uncompressed form
+ */
+ public boolean isUncompressed() {
+ return mMethod == kCompressStored;
+ }
+
+ public AssetFileDescriptor getAssetFileDescriptor() {
+ if (mMethod == kCompressStored) {
+ ParcelFileDescriptor pfd;
+ try {
+ pfd = ParcelFileDescriptor.open(mFile, ParcelFileDescriptor.MODE_READ_ONLY);
+ return new AssetFileDescriptor(pfd, getOffset(), mUncompressedLength);
+ } catch (FileNotFoundException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ }
+ return null;
+ }
+
+ public String getZipFileName() {
+ return mZipFileName;
+ }
+
+ public File getZipFile() {
+ return mFile;
+ }
+
+ }
+
+ private HashMap mHashMap = new HashMap();
+
+ /* for reading compressed files */
+ public HashMap mZipFiles = new HashMap();
+
+ public ZipResourceFile(String zipFileName) throws IOException {
+ addPatchFile(zipFileName);
+ }
+
+ ZipEntryRO[] getEntriesAt(String path) {
+ Vector zev = new Vector();
+ Collection values = mHashMap.values();
+ if (null == path)
+ path = "";
+ int length = path.length();
+ for (ZipEntryRO ze : values) {
+ if (ze.mFileName.startsWith(path)) {
+ if (-1 == ze.mFileName.indexOf('/', length)) {
+ zev.add(ze);
+ }
+ }
+ }
+ ZipEntryRO[] entries = new ZipEntryRO[zev.size()];
+ return zev.toArray(entries);
+ }
+
+ public ZipEntryRO[] getAllEntries() {
+ Collection values = mHashMap.values();
+ return values.toArray(new ZipEntryRO[values.size()]);
+ }
+
+ /**
+ * getAssetFileDescriptor allows for ZipResourceFile to directly feed
+ * Android API's that want an fd, offset, and length such as the
+ * MediaPlayer. It also allows for the class to be used in a content
+ * provider that can feed video players. The file must be stored
+ * (non-compressed) in the Zip file for this to work.
+ *
+ * @param assetPath
+ * @return the asset file descriptor for the file, or null if the file isn't
+ * present or is stored compressed
+ */
+ public AssetFileDescriptor getAssetFileDescriptor(String assetPath) {
+ ZipEntryRO entry = mHashMap.get(assetPath);
+ if (null != entry) {
+ return entry.getAssetFileDescriptor();
+ }
+ return null;
+ }
+
+ /**
+ * getInputStream returns an AssetFileDescriptor.AutoCloseInputStream
+ * associated with the asset that is contained in the Zip file, or a
+ * standard ZipInputStream if necessary to uncompress the file
+ *
+ * @param assetPath
+ * @return an input stream for the named asset path, or null if not found
+ * @throws IOException
+ */
+ public InputStream getInputStream(String assetPath) throws IOException {
+ ZipEntryRO entry = mHashMap.get(assetPath);
+ if (null != entry) {
+ if (entry.isUncompressed()) {
+ return entry.getAssetFileDescriptor().createInputStream();
+ } else {
+ ZipFile zf = mZipFiles.get(entry.getZipFile());
+ /** read compressed files **/
+ if (null == zf) {
+ zf = new ZipFile(entry.getZipFile(), ZipFile.OPEN_READ);
+ mZipFiles.put(entry.getZipFile(), zf);
+ }
+ ZipEntry zi = zf.getEntry(assetPath);
+ if (null != zi)
+ return zf.getInputStream(zi);
+ }
+ }
+ return null;
+ }
+
+ ByteBuffer mLEByteBuffer = ByteBuffer.allocate(4);
+
+ static private int read4LE(RandomAccessFile f) throws EOFException, IOException {
+ return swapEndian(f.readInt());
+ }
+
+ /*
+ * Opens the specified file read-only. We memory-map the entire thing and
+ * close the file before returning.
+ */
+ void addPatchFile(String zipFileName) throws IOException
+ {
+ File file = new File(zipFileName);
+ RandomAccessFile f = new RandomAccessFile(file, "r");
+ long fileLength = f.length();
+
+ if (fileLength < kEOCDLen) {
+ throw new java.io.IOException();
+ }
+
+ long readAmount = kMaxEOCDSearch;
+ if (readAmount > fileLength)
+ readAmount = fileLength;
+
+ /*
+ * Make sure this is a Zip archive.
+ */
+ f.seek(0);
+
+ int header = read4LE(f);
+ if (header == kEOCDSignature) {
+ Log.i(LOG_TAG, "Found Zip archive, but it looks empty");
+ throw new IOException();
+ } else if (header != kLFHSignature) {
+ Log.v(LOG_TAG, "Not a Zip archive");
+ throw new IOException();
+ }
+
+ /*
+ * Perform the traditional EOCD snipe hunt. We're searching for the End
+ * of Central Directory magic number, which appears at the start of the
+ * EOCD block. It's followed by 18 bytes of EOCD stuff and up to 64KB of
+ * archive comment. We need to read the last part of the file into a
+ * buffer, dig through it to find the magic number, parse some values
+ * out, and use those to determine the extent of the CD. We start by
+ * pulling in the last part of the file.
+ */
+ long searchStart = fileLength - readAmount;
+
+ f.seek(searchStart);
+ ByteBuffer bbuf = ByteBuffer.allocate((int) readAmount);
+ byte[] buffer = bbuf.array();
+ f.readFully(buffer);
+ bbuf.order(ByteOrder.LITTLE_ENDIAN);
+
+ /*
+ * Scan backward for the EOCD magic. In an archive without a trailing
+ * comment, we'll find it on the first try. (We may want to consider
+ * doing an initial minimal read; if we don't find it, retry with a
+ * second read as above.)
+ */
+
+ // EOCD == 0x50, 0x4b, 0x05, 0x06
+ int eocdIdx;
+ for (eocdIdx = buffer.length - kEOCDLen; eocdIdx >= 0; eocdIdx--) {
+ if (buffer[eocdIdx] == 0x50 && bbuf.getInt(eocdIdx) == kEOCDSignature)
+ {
+ if (LOGV) {
+ Log.v(LOG_TAG, "+++ Found EOCD at index: " + eocdIdx);
+ }
+ break;
+ }
+ }
+
+ if (eocdIdx < 0) {
+ Log.d(LOG_TAG, "Zip: EOCD not found, " + zipFileName + " is not zip");
+ }
+
+ /*
+ * Grab the CD offset and size, and the number of entries in the
+ * archive. After that, we can release our EOCD hunt buffer.
+ */
+
+ int numEntries = bbuf.getShort(eocdIdx + kEOCDNumEntries);
+ long dirSize = bbuf.getInt(eocdIdx + kEOCDSize) & 0xffffffffL;
+ long dirOffset = bbuf.getInt(eocdIdx + kEOCDFileOffset) & 0xffffffffL;
+
+ // Verify that they look reasonable.
+ if (dirOffset + dirSize > fileLength) {
+ Log.w(LOG_TAG, "bad offsets (dir " + dirOffset + ", size " + dirSize + ", eocd "
+ + eocdIdx + ")");
+ throw new IOException();
+ }
+ if (numEntries == 0) {
+ Log.w(LOG_TAG, "empty archive?");
+ throw new IOException();
+ }
+
+ if (LOGV) {
+ Log.v(LOG_TAG, "+++ numEntries=" + numEntries + " dirSize=" + dirSize + " dirOffset="
+ + dirOffset);
+ }
+
+ MappedByteBuffer directoryMap = f.getChannel()
+ .map(FileChannel.MapMode.READ_ONLY, dirOffset, dirSize);
+ directoryMap.order(ByteOrder.LITTLE_ENDIAN);
+
+ byte[] tempBuf = new byte[0xffff];
+
+ /*
+ * Walk through the central directory, adding entries to the hash table.
+ */
+
+ int currentOffset = 0;
+
+ /*
+ * Allocate the local directory information
+ */
+ ByteBuffer buf = ByteBuffer.allocate(kLFHLen);
+ buf.order(ByteOrder.LITTLE_ENDIAN);
+
+ for (int i = 0; i < numEntries; i++) {
+ if (directoryMap.getInt(currentOffset) != kCDESignature) {
+ Log.w(LOG_TAG, "Missed a central dir sig (at " + currentOffset + ")");
+ throw new IOException();
+ }
+
+ /* useful stuff from the directory entry */
+ int fileNameLen = directoryMap.getShort(currentOffset + kCDENameLen) & 0xffff;
+ int extraLen = directoryMap.getShort(currentOffset + kCDEExtraLen) & 0xffff;
+ int commentLen = directoryMap.getShort(currentOffset + kCDECommentLen) & 0xffff;
+
+ /* get the CDE filename */
+
+ directoryMap.position(currentOffset + kCDELen);
+ directoryMap.get(tempBuf, 0, fileNameLen);
+ directoryMap.position(0);
+
+ /* UTF-8 on Android */
+ String str = new String(tempBuf, 0, fileNameLen);
+ if (LOGV) {
+ Log.v(LOG_TAG, "Filename: " + str);
+ }
+
+ ZipEntryRO ze = new ZipEntryRO(zipFileName, file, str);
+ ze.mMethod = directoryMap.getShort(currentOffset + kCDEMethod) & 0xffff;
+ ze.mWhenModified = directoryMap.getInt(currentOffset + kCDEModWhen) & 0xffffffffL;
+ ze.mCRC32 = directoryMap.getLong(currentOffset + kCDECRC) & 0xffffffffL;
+ ze.mCompressedLength = directoryMap.getLong(currentOffset + kCDECompLen) & 0xffffffffL;
+ ze.mUncompressedLength = directoryMap.getLong(currentOffset + kCDEUncompLen) & 0xffffffffL;
+ ze.mLocalHdrOffset = directoryMap.getInt(currentOffset + kCDELocalOffset) & 0xffffffffL;
+
+ // set the offsets
+ buf.clear();
+ ze.setOffsetFromFile(f, buf);
+
+ // put file into hash
+ mHashMap.put(str, ze);
+
+ // go to next directory entry
+ currentOffset += kCDELen + fileNameLen + extraLen + commentLen;
+ }
+ if (LOGV) {
+ Log.v(LOG_TAG, "+++ zip good scan " + numEntries + " entries");
+ }
+ }
+}
diff --git a/android/src/main/java/com/brentvatne/react/ReactVideoView.java b/android/src/main/java/com/brentvatne/react/ReactVideoView.java
index 5b002d4c..40f9abbc 100644
--- a/android/src/main/java/com/brentvatne/react/ReactVideoView.java
+++ b/android/src/main/java/com/brentvatne/react/ReactVideoView.java
@@ -1,5 +1,6 @@
package com.brentvatne.react;
+import android.content.res.AssetFileDescriptor;
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.Handler;
@@ -16,6 +17,10 @@ import com.facebook.react.uimanager.events.RCTEventEmitter;
import com.yqritc.scalablevideoview.ScalableType;
import com.yqritc.scalablevideoview.ScalableVideoView;
+import com.android.vending.expansion.zipfile.APKExpansionSupport;
+import com.android.vending.expansion.zipfile.ZipResourceFile;
+
+import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@@ -86,7 +91,11 @@ public class ReactVideoView extends ScalableVideoView implements MediaPlayer.OnP
private float mRate = 1.0f;
private boolean mPlayInBackground = false;
+ private int mMainVer = 0;
+ private int mPatchVer = 0;
+
private boolean mMediaPlayerValid = false; // True if mMediaPlayer is in prepared, started, paused or completed state.
+
private int mVideoDuration = 0;
private int mVideoBufferedDuration = 0;
private boolean isCompleted = false;
@@ -160,11 +169,18 @@ public class ReactVideoView extends ScalableVideoView implements MediaPlayer.OnP
}
public void setSrc(final String uriString, final String type, final boolean isNetwork, final boolean isAsset) {
+ setSrc(uriString,type,isNetwork,isAsset,0,0);
+ }
+
+ public void setSrc(final String uriString, final String type, final boolean isNetwork, final boolean isAsset, final int expansionMainVersion, final int expansionPatchVersion) {
mSrcUriString = uriString;
mSrcType = type;
mSrcIsNetwork = isNetwork;
mSrcIsAsset = isAsset;
+ mMainVer = expansionMainVersion;
+ mPatchVer = expansionPatchVersion;
+
mMediaPlayerValid = false;
mVideoDuration = 0;
@@ -199,11 +215,28 @@ public class ReactVideoView extends ScalableVideoView implements MediaPlayer.OnP
setDataSource(uriString);
}
} else {
- setRawData(mThemedReactContext.getResources().getIdentifier(
- uriString,
- "raw",
- mThemedReactContext.getPackageName()
- ));
+ ZipResourceFile expansionFile= null;
+ AssetFileDescriptor fd= null;
+ if(mMainVer>0) {
+ try {
+ expansionFile = APKExpansionSupport.getAPKExpansionZipFile(mThemedReactContext, mMainVer, mPatchVer);
+ fd = expansionFile.getAssetFileDescriptor(uriString.replace(".mp4","") + ".mp4");
+ } catch (IOException e) {
+ e.printStackTrace();
+ } catch (NullPointerException e) {
+ e.printStackTrace();
+ }
+ }
+ if(fd==null) {
+ setRawData(mThemedReactContext.getResources().getIdentifier(
+ uriString,
+ "raw",
+ mThemedReactContext.getPackageName()
+ ));
+ }
+ else {
+ setDataSource(fd.getFileDescriptor(), fd.getStartOffset(),fd.getLength());
+ }
}
} catch (Exception e) {
e.printStackTrace();
@@ -214,11 +247,18 @@ public class ReactVideoView extends ScalableVideoView implements MediaPlayer.OnP
src.putString(ReactVideoViewManager.PROP_SRC_URI, uriString);
src.putString(ReactVideoViewManager.PROP_SRC_TYPE, type);
src.putBoolean(ReactVideoViewManager.PROP_SRC_IS_NETWORK, isNetwork);
+ if(mMainVer>0) {
+ src.putInt(ReactVideoViewManager.PROP_SRC_MAINVER, mMainVer);
+ if(mPatchVer>0) {
+ src.putInt(ReactVideoViewManager.PROP_SRC_PATCHVER, mPatchVer);
+ }
+ }
WritableMap event = Arguments.createMap();
event.putMap(ReactVideoViewManager.PROP_SRC, src);
mEventEmitter.receiveEvent(getId(), Events.EVENT_LOAD_START.toString(), event);
- prepareAsync(this);
+ // not async to prevent random crashes on Android playback from local resource due to race conditions
+ prepare(this);
}
public void setResizeModeModifier(final ScalableType resizeMode) {
@@ -443,7 +483,14 @@ public class ReactVideoView extends ScalableVideoView implements MediaPlayer.OnP
protected void onAttachedToWindow() {
super.onAttachedToWindow();
- setSrc(mSrcUriString, mSrcType, mSrcIsNetwork, mSrcIsAsset);
+
+ if(mMainVer>0) {
+ setSrc(mSrcUriString, mSrcType, mSrcIsNetwork,mSrcIsAsset,mMainVer,mPatchVer);
+ }
+ else {
+ setSrc(mSrcUriString, mSrcType, mSrcIsNetwork,mSrcIsAsset);
+ }
+
}
@Override
diff --git a/android/src/main/java/com/brentvatne/react/ReactVideoViewManager.java b/android/src/main/java/com/brentvatne/react/ReactVideoViewManager.java
index 7ab2bddf..942d9277 100644
--- a/android/src/main/java/com/brentvatne/react/ReactVideoViewManager.java
+++ b/android/src/main/java/com/brentvatne/react/ReactVideoViewManager.java
@@ -22,6 +22,8 @@ public class ReactVideoViewManager extends SimpleViewManager {
public static final String PROP_SRC_URI = "uri";
public static final String PROP_SRC_TYPE = "type";
public static final String PROP_SRC_IS_NETWORK = "isNetwork";
+ public static final String PROP_SRC_MAINVER = "mainVer";
+ public static final String PROP_SRC_PATCHVER = "patchVer";
public static final String PROP_SRC_IS_ASSET = "isAsset";
public static final String PROP_RESIZE_MODE = "resizeMode";
public static final String PROP_REPEAT = "repeat";
@@ -72,12 +74,28 @@ public class ReactVideoViewManager extends SimpleViewManager {
@ReactProp(name = PROP_SRC)
public void setSrc(final ReactVideoView videoView, @Nullable ReadableMap src) {
- videoView.setSrc(
- src.getString(PROP_SRC_URI),
- src.getString(PROP_SRC_TYPE),
- src.getBoolean(PROP_SRC_IS_NETWORK),
- src.getBoolean(PROP_SRC_IS_ASSET)
- );
+ int mainVer = src.getInt(PROP_SRC_MAINVER);
+ int patchVer = src.getInt(PROP_SRC_PATCHVER);
+ if(mainVer<0) { mainVer = 0; }
+ if(patchVer<0) { patchVer = 0; }
+ if(mainVer>0) {
+ videoView.setSrc(
+ src.getString(PROP_SRC_URI),
+ src.getString(PROP_SRC_TYPE),
+ src.getBoolean(PROP_SRC_IS_NETWORK),,
+ src.getBoolean(PROP_SRC_IS_ASSET)
+ mainVer,
+ patchVer
+ );
+ }
+ else {
+ videoView.setSrc(
+ src.getString(PROP_SRC_URI),
+ src.getString(PROP_SRC_TYPE),
+ src.getBoolean(PROP_SRC_IS_NETWORK),
+ src.getBoolean(PROP_SRC_IS_ASSET)
+ );
+ }
}
@ReactProp(name = PROP_RESIZE_MODE)