Merge branch 'master' of https://github.com/react-native-video/react-native-video into fix/report_time_position_when_updated
# Conflicts: # CHANGELOG.md
This commit is contained in:
@@ -1,287 +0,0 @@
|
||||
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.
|
||||
*
|
||||
* <p>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<ContentProviderOperation> 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;
|
||||
}
|
||||
|
||||
}
|
@@ -1,81 +0,0 @@
|
||||
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<String> ret = new Vector<String>();
|
||||
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);
|
||||
}
|
||||
}
|
@@ -1,428 +0,0 @@
|
||||
|
||||
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<String, ZipEntryRO> mHashMap = new HashMap<String, ZipEntryRO>();
|
||||
|
||||
/* for reading compressed files */
|
||||
public HashMap<File, ZipFile> mZipFiles = new HashMap<File, ZipFile>();
|
||||
|
||||
public ZipResourceFile(String zipFileName) throws IOException {
|
||||
addPatchFile(zipFileName);
|
||||
}
|
||||
|
||||
ZipEntryRO[] getEntriesAt(String path) {
|
||||
Vector<ZipEntryRO> zev = new Vector<ZipEntryRO>();
|
||||
Collection<ZipEntryRO> 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<ZipEntryRO> 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");
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* Copyright (C) 2016 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.
|
||||
*/
|
||||
package com.brentvatne.exoplayer;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
/**
|
||||
* A {@link FrameLayout} that resizes itself to match a specified aspect ratio.
|
||||
*/
|
||||
public final class AspectRatioFrameLayout extends FrameLayout {
|
||||
|
||||
/**
|
||||
* The {@link FrameLayout} will not resize itself if the fractional difference between its natural
|
||||
* aspect ratio and the requested aspect ratio falls below this threshold.
|
||||
* <p>
|
||||
* This tolerance allows the view to occupy the whole of the screen when the requested aspect
|
||||
* ratio is very close, but not exactly equal to, the aspect ratio of the screen. This may reduce
|
||||
* the number of view layers that need to be composited by the underlying system, which can help
|
||||
* to reduce power consumption.
|
||||
*/
|
||||
private static final float MAX_ASPECT_RATIO_DEFORMATION_FRACTION = 0.01f;
|
||||
|
||||
private float videoAspectRatio;
|
||||
private @ResizeMode.Mode int resizeMode = ResizeMode.RESIZE_MODE_FIT;
|
||||
|
||||
public AspectRatioFrameLayout(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public AspectRatioFrameLayout(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the aspect ratio that this view should satisfy.
|
||||
*
|
||||
* @param widthHeightRatio The width to height ratio.
|
||||
*/
|
||||
public void setAspectRatio(float widthHeightRatio) {
|
||||
if (this.videoAspectRatio != widthHeightRatio) {
|
||||
this.videoAspectRatio = widthHeightRatio;
|
||||
requestLayout();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the aspect ratio that this view should satisfy.
|
||||
*
|
||||
* @return widthHeightRatio The width to height ratio.
|
||||
*/
|
||||
public float getAspectRatio() {
|
||||
return videoAspectRatio;
|
||||
}
|
||||
|
||||
public void invalidateAspectRatio() {
|
||||
videoAspectRatio = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the resize mode which can be of value {@link ResizeMode.Mode}
|
||||
*
|
||||
* @param resizeMode The resize mode.
|
||||
*/
|
||||
public void setResizeMode(@ResizeMode.Mode int resizeMode) {
|
||||
if (this.resizeMode != resizeMode) {
|
||||
this.resizeMode = resizeMode;
|
||||
requestLayout();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the resize mode which can be of value {@link ResizeMode.Mode}
|
||||
*
|
||||
* @return resizeMode The resize mode.
|
||||
*/
|
||||
public @ResizeMode.Mode int getResizeMode() {
|
||||
return resizeMode;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
if (videoAspectRatio == 0) {
|
||||
// Aspect ratio not set.
|
||||
return;
|
||||
}
|
||||
|
||||
int measuredWidth = getMeasuredWidth();
|
||||
int measuredHeight = getMeasuredHeight();
|
||||
int width = measuredWidth;
|
||||
int height = measuredHeight;
|
||||
|
||||
float viewAspectRatio = (float) measuredWidth / measuredHeight;
|
||||
float aspectDeformation = videoAspectRatio / viewAspectRatio - 1;
|
||||
if (Math.abs(aspectDeformation) <= MAX_ASPECT_RATIO_DEFORMATION_FRACTION) {
|
||||
// We're within the allowed tolerance.
|
||||
return;
|
||||
}
|
||||
|
||||
switch (resizeMode) {
|
||||
case ResizeMode.RESIZE_MODE_FIXED_WIDTH:
|
||||
height = (int) (measuredWidth / videoAspectRatio);
|
||||
break;
|
||||
case ResizeMode.RESIZE_MODE_FIXED_HEIGHT:
|
||||
width = (int) (measuredHeight * videoAspectRatio);
|
||||
break;
|
||||
case ResizeMode.RESIZE_MODE_FILL:
|
||||
// Do nothing width and height is the same as the view
|
||||
break;
|
||||
case ResizeMode.RESIZE_MODE_CENTER_CROP:
|
||||
width = (int) (measuredHeight * videoAspectRatio);
|
||||
|
||||
// Scale video if it doesn't fill the measuredWidth
|
||||
if (width < measuredWidth) {
|
||||
float scaleFactor = (float) measuredWidth / width;
|
||||
width = (int) (width * scaleFactor);
|
||||
height = (int) (measuredHeight * scaleFactor);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (aspectDeformation > 0) {
|
||||
height = (int) (measuredWidth / videoAspectRatio);
|
||||
} else {
|
||||
width = (int) (measuredHeight * videoAspectRatio);
|
||||
}
|
||||
break;
|
||||
}
|
||||
super.onMeasure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
|
||||
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,97 @@
|
||||
package com.brentvatne.exoplayer;
|
||||
|
||||
import com.facebook.react.bridge.ReactContext;
|
||||
import com.facebook.react.modules.network.CookieJarContainer;
|
||||
import com.facebook.react.modules.network.ForwardingCookieHandler;
|
||||
import com.facebook.react.modules.network.OkHttpClientProvider;
|
||||
import com.google.android.exoplayer2.ext.okhttp.OkHttpDataSource;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSource;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
|
||||
import okhttp3.Call;
|
||||
import okhttp3.JavaNetCookieJar;
|
||||
import okhttp3.OkHttpClient;
|
||||
import java.util.Map;
|
||||
|
||||
public class DataSourceUtil {
|
||||
|
||||
private DataSourceUtil() {
|
||||
}
|
||||
|
||||
private static DataSource.Factory rawDataSourceFactory = null;
|
||||
private static DataSource.Factory defaultDataSourceFactory = null;
|
||||
private static HttpDataSource.Factory defaultHttpDataSourceFactory = null;
|
||||
private static String userAgent = null;
|
||||
|
||||
public static void setUserAgent(String userAgent) {
|
||||
DataSourceUtil.userAgent = userAgent;
|
||||
}
|
||||
|
||||
public static String getUserAgent(ReactContext context) {
|
||||
if (userAgent == null) {
|
||||
userAgent = Util.getUserAgent(context, "ReactNativeVideo");
|
||||
}
|
||||
return userAgent;
|
||||
}
|
||||
|
||||
public static DataSource.Factory getRawDataSourceFactory(ReactContext context) {
|
||||
if (rawDataSourceFactory == null) {
|
||||
rawDataSourceFactory = buildRawDataSourceFactory(context);
|
||||
}
|
||||
return rawDataSourceFactory;
|
||||
}
|
||||
|
||||
public static void setRawDataSourceFactory(DataSource.Factory factory) {
|
||||
DataSourceUtil.rawDataSourceFactory = factory;
|
||||
}
|
||||
|
||||
|
||||
public static DataSource.Factory getDefaultDataSourceFactory(ReactContext context, DefaultBandwidthMeter bandwidthMeter, Map<String, String> requestHeaders) {
|
||||
if (defaultDataSourceFactory == null || (requestHeaders != null && !requestHeaders.isEmpty())) {
|
||||
defaultDataSourceFactory = buildDataSourceFactory(context, bandwidthMeter, requestHeaders);
|
||||
}
|
||||
return defaultDataSourceFactory;
|
||||
}
|
||||
|
||||
public static void setDefaultDataSourceFactory(DataSource.Factory factory) {
|
||||
DataSourceUtil.defaultDataSourceFactory = factory;
|
||||
}
|
||||
|
||||
public static HttpDataSource.Factory getDefaultHttpDataSourceFactory(ReactContext context, DefaultBandwidthMeter bandwidthMeter, Map<String, String> requestHeaders) {
|
||||
if (defaultHttpDataSourceFactory == null || (requestHeaders != null && !requestHeaders.isEmpty())) {
|
||||
defaultHttpDataSourceFactory = buildHttpDataSourceFactory(context, bandwidthMeter, requestHeaders);
|
||||
}
|
||||
return defaultHttpDataSourceFactory;
|
||||
}
|
||||
|
||||
public static void setDefaultHttpDataSourceFactory(HttpDataSource.Factory factory) {
|
||||
DataSourceUtil.defaultHttpDataSourceFactory = factory;
|
||||
}
|
||||
|
||||
private static DataSource.Factory buildRawDataSourceFactory(ReactContext context) {
|
||||
return new RawResourceDataSourceFactory(context.getApplicationContext());
|
||||
}
|
||||
|
||||
private static DataSource.Factory buildDataSourceFactory(ReactContext context, DefaultBandwidthMeter bandwidthMeter, Map<String, String> requestHeaders) {
|
||||
return new DefaultDataSource.Factory(context,
|
||||
buildHttpDataSourceFactory(context, bandwidthMeter, requestHeaders));
|
||||
}
|
||||
|
||||
private static HttpDataSource.Factory buildHttpDataSourceFactory(ReactContext context, DefaultBandwidthMeter bandwidthMeter, Map<String, String> requestHeaders) {
|
||||
OkHttpClient client = OkHttpClientProvider.getOkHttpClient();
|
||||
CookieJarContainer container = (CookieJarContainer) client.cookieJar();
|
||||
ForwardingCookieHandler handler = new ForwardingCookieHandler(context);
|
||||
container.setCookieJar(new JavaNetCookieJar(handler));
|
||||
OkHttpDataSource.Factory okHttpDataSourceFactory = new OkHttpDataSource.Factory((Call.Factory) client)
|
||||
.setUserAgent(getUserAgent(context))
|
||||
.setTransferListener(bandwidthMeter);
|
||||
|
||||
if (requestHeaders != null)
|
||||
okHttpDataSourceFactory.setDefaultRequestProperties(requestHeaders);
|
||||
|
||||
return okHttpDataSourceFactory;
|
||||
}
|
||||
}
|
@@ -0,0 +1,38 @@
|
||||
package com.brentvatne.exoplayer;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
|
||||
import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy;
|
||||
import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
|
||||
|
||||
public class DefaultReactExoplayerConfig implements ReactExoplayerConfig {
|
||||
|
||||
private final DefaultBandwidthMeter bandwidthMeter;
|
||||
private boolean disableDisconnectError = false;
|
||||
|
||||
public DefaultReactExoplayerConfig(Context context) {
|
||||
this.bandwidthMeter = new DefaultBandwidthMeter.Builder(context).build();
|
||||
}
|
||||
|
||||
public LoadErrorHandlingPolicy buildLoadErrorHandlingPolicy(int minLoadRetryCount) {
|
||||
if (this.disableDisconnectError) {
|
||||
// Use custom error handling policy to prevent throwing an error when losing network connection
|
||||
return new ReactExoplayerLoadErrorHandlingPolicy(minLoadRetryCount);
|
||||
}
|
||||
return new DefaultLoadErrorHandlingPolicy(minLoadRetryCount);
|
||||
}
|
||||
|
||||
public void setDisableDisconnectError(boolean disableDisconnectError) {
|
||||
this.disableDisconnectError = disableDisconnectError;
|
||||
}
|
||||
|
||||
public boolean getDisableDisconnectError() {
|
||||
return this.disableDisconnectError;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultBandwidthMeter getBandwidthMeter() {
|
||||
return bandwidthMeter;
|
||||
}
|
||||
}
|
@@ -0,0 +1,307 @@
|
||||
package com.brentvatne.exoplayer;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.Gravity;
|
||||
import android.view.SurfaceView;
|
||||
import android.view.TextureView;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.PlaybackException;
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.TracksInfo;
|
||||
import com.google.android.exoplayer2.text.Cue;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||
import com.google.android.exoplayer2.ui.SubtitleView;
|
||||
import com.google.android.exoplayer2.video.VideoSize;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@TargetApi(16)
|
||||
public final class ExoPlayerView extends FrameLayout {
|
||||
|
||||
private View surfaceView;
|
||||
private final View shutterView;
|
||||
private final SubtitleView subtitleLayout;
|
||||
private final AspectRatioFrameLayout layout;
|
||||
private final ComponentListener componentListener;
|
||||
private ExoPlayer player;
|
||||
private Context context;
|
||||
private ViewGroup.LayoutParams layoutParams;
|
||||
|
||||
private boolean useTextureView = true;
|
||||
private boolean useSecureView = false;
|
||||
private boolean hideShutterView = false;
|
||||
|
||||
public ExoPlayerView(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public ExoPlayerView(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public ExoPlayerView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
|
||||
this.context = context;
|
||||
|
||||
layoutParams = new ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT);
|
||||
|
||||
componentListener = new ComponentListener();
|
||||
|
||||
FrameLayout.LayoutParams aspectRatioParams = new FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
FrameLayout.LayoutParams.MATCH_PARENT);
|
||||
aspectRatioParams.gravity = Gravity.CENTER;
|
||||
layout = new AspectRatioFrameLayout(context);
|
||||
layout.setLayoutParams(aspectRatioParams);
|
||||
|
||||
shutterView = new View(getContext());
|
||||
shutterView.setLayoutParams(layoutParams);
|
||||
shutterView.setBackgroundColor(ContextCompat.getColor(context, android.R.color.black));
|
||||
|
||||
subtitleLayout = new SubtitleView(context);
|
||||
subtitleLayout.setLayoutParams(layoutParams);
|
||||
subtitleLayout.setUserDefaultStyle();
|
||||
subtitleLayout.setUserDefaultTextSize();
|
||||
|
||||
updateSurfaceView();
|
||||
|
||||
layout.addView(shutterView, 1, layoutParams);
|
||||
layout.addView(subtitleLayout, 2, layoutParams);
|
||||
|
||||
addViewInLayout(layout, 0, aspectRatioParams);
|
||||
}
|
||||
|
||||
private void clearVideoView() {
|
||||
if (surfaceView instanceof TextureView) {
|
||||
player.clearVideoTextureView((TextureView) surfaceView);
|
||||
} else if (surfaceView instanceof SurfaceView) {
|
||||
player.clearVideoSurfaceView((SurfaceView) surfaceView);
|
||||
}
|
||||
}
|
||||
|
||||
private void setVideoView() {
|
||||
if (surfaceView instanceof TextureView) {
|
||||
player.setVideoTextureView((TextureView) surfaceView);
|
||||
} else if (surfaceView instanceof SurfaceView) {
|
||||
player.setVideoSurfaceView((SurfaceView) surfaceView);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateSurfaceView() {
|
||||
View view;
|
||||
if (!useTextureView || useSecureView) {
|
||||
view = new SurfaceView(context);
|
||||
if (useSecureView) {
|
||||
((SurfaceView)view).setSecure(true);
|
||||
}
|
||||
} else {
|
||||
view = new TextureView(context);
|
||||
}
|
||||
view.setLayoutParams(layoutParams);
|
||||
|
||||
surfaceView = view;
|
||||
if (layout.getChildAt(0) != null) {
|
||||
layout.removeViewAt(0);
|
||||
}
|
||||
layout.addView(surfaceView, 0, layoutParams);
|
||||
|
||||
if (this.player != null) {
|
||||
setVideoView();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateShutterViewVisibility() {
|
||||
shutterView.setVisibility(this.hideShutterView ? View.INVISIBLE : View.VISIBLE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@link ExoPlayer} to use. The {@link ExoPlayer#addListener} method of the
|
||||
* player will be called and previous
|
||||
* assignments are overridden.
|
||||
*
|
||||
* @param player The {@link ExoPlayer} to use.
|
||||
*/
|
||||
public void setPlayer(ExoPlayer player) {
|
||||
if (this.player == player) {
|
||||
return;
|
||||
}
|
||||
if (this.player != null) {
|
||||
this.player.removeListener(componentListener);
|
||||
clearVideoView();
|
||||
}
|
||||
this.player = player;
|
||||
shutterView.setVisibility(this.hideShutterView ? View.INVISIBLE : View.VISIBLE);
|
||||
if (player != null) {
|
||||
setVideoView();
|
||||
player.addListener(componentListener);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the resize mode which can be of value {@link ResizeMode.Mode}
|
||||
*
|
||||
* @param resizeMode The resize mode.
|
||||
*/
|
||||
public void setResizeMode(@ResizeMode.Mode int resizeMode) {
|
||||
if (layout.getResizeMode() != resizeMode) {
|
||||
layout.setResizeMode(resizeMode);
|
||||
post(measureAndLayout);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the view onto which video is rendered. This is either a {@link SurfaceView} (default)
|
||||
* or a {@link TextureView} if the {@code use_texture_view} view attribute has been set to true.
|
||||
*
|
||||
* @return either a {@link SurfaceView} or a {@link TextureView}.
|
||||
*/
|
||||
public View getVideoSurfaceView() {
|
||||
return surfaceView;
|
||||
}
|
||||
|
||||
public void setUseTextureView(boolean useTextureView) {
|
||||
if (useTextureView != this.useTextureView) {
|
||||
this.useTextureView = useTextureView;
|
||||
updateSurfaceView();
|
||||
}
|
||||
}
|
||||
|
||||
public void useSecureView(boolean useSecureView) {
|
||||
if (useSecureView != this.useSecureView) {
|
||||
this.useSecureView = useSecureView;
|
||||
updateSurfaceView();
|
||||
}
|
||||
}
|
||||
|
||||
public void setHideShutterView(boolean hideShutterView) {
|
||||
this.hideShutterView = hideShutterView;
|
||||
updateShutterViewVisibility();
|
||||
}
|
||||
|
||||
private final Runnable measureAndLayout = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
measure(
|
||||
MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.EXACTLY),
|
||||
MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.EXACTLY));
|
||||
layout(getLeft(), getTop(), getRight(), getBottom());
|
||||
}
|
||||
};
|
||||
|
||||
private void updateForCurrentTrackSelections() {
|
||||
if (player == null) {
|
||||
return;
|
||||
}
|
||||
TrackSelectionArray selections = player.getCurrentTrackSelections();
|
||||
for (int i = 0; i < selections.length; i++) {
|
||||
if (player.getRendererType(i) == C.TRACK_TYPE_VIDEO && selections.get(i) != null) {
|
||||
// Video enabled so artwork must be hidden. If the shutter is closed, it will be opened in
|
||||
// onRenderedFirstFrame().
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Video disabled so the shutter must be closed.
|
||||
shutterView.setVisibility(this.hideShutterView ? View.INVISIBLE : View.VISIBLE);
|
||||
}
|
||||
|
||||
public void invalidateAspectRatio() {
|
||||
// Resetting aspect ratio will force layout refresh on next video size changed
|
||||
layout.invalidateAspectRatio();
|
||||
}
|
||||
|
||||
private final class ComponentListener implements Player.Listener {
|
||||
|
||||
// TextRenderer.Output implementation
|
||||
|
||||
@Override
|
||||
public void onCues(List<Cue> cues) {
|
||||
subtitleLayout.onCues(cues);
|
||||
}
|
||||
|
||||
// ExoPlayer.VideoListener implementation
|
||||
|
||||
@Override
|
||||
public void onVideoSizeChanged(VideoSize videoSize) {
|
||||
boolean isInitialRatio = layout.getAspectRatio() == 0;
|
||||
layout.setAspectRatio(videoSize.height == 0 ? 1 : (videoSize.width * videoSize.pixelWidthHeightRatio) / videoSize.height);
|
||||
|
||||
// React native workaround for measuring and layout on initial load.
|
||||
if (isInitialRatio) {
|
||||
post(measureAndLayout);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRenderedFirstFrame() {
|
||||
shutterView.setVisibility(INVISIBLE);
|
||||
}
|
||||
|
||||
// ExoPlayer.EventListener implementation
|
||||
|
||||
@Override
|
||||
public void onIsLoadingChanged(boolean isLoading) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaybackStateChanged(int playbackState) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayWhenReadyChanged(boolean playWhenReady, int reason) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerError(PlaybackException e) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPositionDiscontinuity(Player.PositionInfo oldPosition, Player.PositionInfo newPosition, int reason) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTimelineChanged(Timeline timeline, int reason) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTracksInfoChanged(TracksInfo tracksInfo) {
|
||||
updateForCurrentTrackSelections();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaybackParametersChanged(PlaybackParameters params) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRepeatModeChanged(int repeatMode) {
|
||||
// Do nothing.
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,20 @@
|
||||
package com.brentvatne.exoplayer;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.RawResourceDataSource;
|
||||
|
||||
class RawResourceDataSourceFactory implements DataSource.Factory {
|
||||
|
||||
private final Context context;
|
||||
|
||||
RawResourceDataSourceFactory(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DataSource createDataSource() {
|
||||
return new RawResourceDataSource(context);
|
||||
}
|
||||
}
|
@@ -0,0 +1,16 @@
|
||||
package com.brentvatne.exoplayer;
|
||||
|
||||
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
|
||||
import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
|
||||
|
||||
/**
|
||||
* Extension points to configure the Exoplayer instance
|
||||
*/
|
||||
public interface ReactExoplayerConfig {
|
||||
LoadErrorHandlingPolicy buildLoadErrorHandlingPolicy(int minLoadRetryCount);
|
||||
|
||||
void setDisableDisconnectError(boolean disableDisconnectError);
|
||||
boolean getDisableDisconnectError();
|
||||
|
||||
DefaultBandwidthMeter getBandwidthMeter();
|
||||
}
|
@@ -0,0 +1,36 @@
|
||||
package com.brentvatne.exoplayer;
|
||||
|
||||
import java.io.IOException;
|
||||
import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource.HttpDataSourceException;
|
||||
import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy.LoadErrorInfo;
|
||||
import com.google.android.exoplayer2.C;
|
||||
|
||||
public final class ReactExoplayerLoadErrorHandlingPolicy extends DefaultLoadErrorHandlingPolicy {
|
||||
private int minLoadRetryCount = Integer.MAX_VALUE;
|
||||
|
||||
public ReactExoplayerLoadErrorHandlingPolicy(int minLoadRetryCount) {
|
||||
super(minLoadRetryCount);
|
||||
this.minLoadRetryCount = minLoadRetryCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getRetryDelayMsFor(LoadErrorInfo loadErrorInfo) {
|
||||
if (
|
||||
loadErrorInfo.exception instanceof HttpDataSourceException &&
|
||||
(loadErrorInfo.exception.getMessage() == "Unable to connect" || loadErrorInfo.exception.getMessage() == "Software caused connection abort")
|
||||
) {
|
||||
// Capture the error we get when there is no network connectivity and keep retrying it
|
||||
return 1000; // Retry every second
|
||||
} else if(loadErrorInfo.errorCount < this.minLoadRetryCount) {
|
||||
return Math.min((loadErrorInfo.errorCount - 1) * 1000, 5000); // Default timeout handling
|
||||
} else {
|
||||
return C.TIME_UNSET; // Done retrying and will return the error immediately
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMinimumLoadableRetryCount(int dataType) {
|
||||
return Integer.MAX_VALUE;
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,418 @@
|
||||
package com.brentvatne.exoplayer;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import com.facebook.react.bridge.Dynamic;
|
||||
import com.facebook.react.bridge.ReadableArray;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.react.bridge.ReadableMapKeySetIterator;
|
||||
import com.facebook.react.common.MapBuilder;
|
||||
import com.facebook.react.uimanager.ThemedReactContext;
|
||||
import com.facebook.react.uimanager.ViewGroupManager;
|
||||
import com.facebook.react.uimanager.annotations.ReactProp;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import com.google.android.exoplayer2.DefaultLoadControl;
|
||||
import com.google.android.exoplayer2.upstream.RawResourceDataSource;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerView> {
|
||||
|
||||
private static final String REACT_CLASS = "RCTVideo";
|
||||
|
||||
private static final String PROP_SRC = "src";
|
||||
private static final String PROP_SRC_URI = "uri";
|
||||
private static final String PROP_SRC_TYPE = "type";
|
||||
private static final String PROP_DRM = "drm";
|
||||
private static final String PROP_DRM_TYPE = "type";
|
||||
private static final String PROP_DRM_LICENSESERVER = "licenseServer";
|
||||
private static final String PROP_DRM_HEADERS = "headers";
|
||||
private static final String PROP_SRC_HEADERS = "requestHeaders";
|
||||
private static final String PROP_RESIZE_MODE = "resizeMode";
|
||||
private static final String PROP_REPEAT = "repeat";
|
||||
private static final String PROP_SELECTED_AUDIO_TRACK = "selectedAudioTrack";
|
||||
private static final String PROP_SELECTED_AUDIO_TRACK_TYPE = "type";
|
||||
private static final String PROP_SELECTED_AUDIO_TRACK_VALUE = "value";
|
||||
private static final String PROP_SELECTED_TEXT_TRACK = "selectedTextTrack";
|
||||
private static final String PROP_SELECTED_TEXT_TRACK_TYPE = "type";
|
||||
private static final String PROP_SELECTED_TEXT_TRACK_VALUE = "value";
|
||||
private static final String PROP_TEXT_TRACKS = "textTracks";
|
||||
private static final String PROP_PAUSED = "paused";
|
||||
private static final String PROP_MUTED = "muted";
|
||||
private static final String PROP_VOLUME = "volume";
|
||||
private static final String PROP_BACK_BUFFER_DURATION_MS = "backBufferDurationMs";
|
||||
private static final String PROP_BUFFER_CONFIG = "bufferConfig";
|
||||
private static final String PROP_BUFFER_CONFIG_MIN_BUFFER_MS = "minBufferMs";
|
||||
private static final String PROP_BUFFER_CONFIG_MAX_BUFFER_MS = "maxBufferMs";
|
||||
private static final String PROP_BUFFER_CONFIG_BUFFER_FOR_PLAYBACK_MS = "bufferForPlaybackMs";
|
||||
private static final String PROP_BUFFER_CONFIG_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = "bufferForPlaybackAfterRebufferMs";
|
||||
private static final String PROP_BUFFER_CONFIG_MAX_HEAP_ALLOCATION_PERCENT = "maxHeapAllocationPercent";
|
||||
private static final String PROP_BUFFER_CONFIG_MIN_BACK_BUFFER_MEMORY_RESERVE_PERCENT = "minBackBufferMemoryReservePercent";
|
||||
private static final String PROP_BUFFER_CONFIG_MIN_BUFFER_MEMORY_RESERVE_PERCENT = "minBufferMemoryReservePercent";
|
||||
private static final String PROP_PREVENTS_DISPLAY_SLEEP_DURING_VIDEO_PLAYBACK = "preventsDisplaySleepDuringVideoPlayback";
|
||||
private static final String PROP_PROGRESS_UPDATE_INTERVAL = "progressUpdateInterval";
|
||||
private static final String PROP_REPORT_BANDWIDTH = "reportBandwidth";
|
||||
private static final String PROP_SEEK = "seek";
|
||||
private static final String PROP_RATE = "rate";
|
||||
private static final String PROP_MIN_LOAD_RETRY_COUNT = "minLoadRetryCount";
|
||||
private static final String PROP_MAXIMUM_BIT_RATE = "maxBitRate";
|
||||
private static final String PROP_PLAY_IN_BACKGROUND = "playInBackground";
|
||||
private static final String PROP_CONTENT_START_TIME = "contentStartTime";
|
||||
private static final String PROP_DISABLE_FOCUS = "disableFocus";
|
||||
private static final String PROP_DISABLE_BUFFERING = "disableBuffering";
|
||||
private static final String PROP_DISABLE_DISCONNECT_ERROR = "disableDisconnectError";
|
||||
private static final String PROP_FULLSCREEN = "fullscreen";
|
||||
private static final String PROP_USE_TEXTURE_VIEW = "useTextureView";
|
||||
private static final String PROP_SECURE_VIEW = "useSecureView";
|
||||
private static final String PROP_SELECTED_VIDEO_TRACK = "selectedVideoTrack";
|
||||
private static final String PROP_SELECTED_VIDEO_TRACK_TYPE = "type";
|
||||
private static final String PROP_SELECTED_VIDEO_TRACK_VALUE = "value";
|
||||
private static final String PROP_HIDE_SHUTTER_VIEW = "hideShutterView";
|
||||
private static final String PROP_CONTROLS = "controls";
|
||||
|
||||
private ReactExoplayerConfig config;
|
||||
|
||||
public ReactExoplayerViewManager(ReactExoplayerConfig config) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return REACT_CLASS;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ReactExoplayerView createViewInstance(ThemedReactContext themedReactContext) {
|
||||
return new ReactExoplayerView(themedReactContext, config);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDropViewInstance(ReactExoplayerView view) {
|
||||
view.cleanUpResources();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Map<String, Object> getExportedCustomDirectEventTypeConstants() {
|
||||
MapBuilder.Builder<String, Object> builder = MapBuilder.builder();
|
||||
for (String event : VideoEventEmitter.Events) {
|
||||
builder.put(event, MapBuilder.of("registrationName", event));
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Map<String, Object> getExportedViewConstants() {
|
||||
return MapBuilder.<String, Object>of(
|
||||
"ScaleNone", Integer.toString(ResizeMode.RESIZE_MODE_FIT),
|
||||
"ScaleAspectFit", Integer.toString(ResizeMode.RESIZE_MODE_FIT),
|
||||
"ScaleToFill", Integer.toString(ResizeMode.RESIZE_MODE_FILL),
|
||||
"ScaleAspectFill", Integer.toString(ResizeMode.RESIZE_MODE_CENTER_CROP)
|
||||
);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_DRM)
|
||||
public void setDRM(final ReactExoplayerView videoView, @Nullable ReadableMap drm) {
|
||||
if (drm != null && drm.hasKey(PROP_DRM_TYPE)) {
|
||||
String drmType = drm.hasKey(PROP_DRM_TYPE) ? drm.getString(PROP_DRM_TYPE) : null;
|
||||
String drmLicenseServer = drm.hasKey(PROP_DRM_LICENSESERVER) ? drm.getString(PROP_DRM_LICENSESERVER) : null;
|
||||
ReadableMap drmHeaders = drm.hasKey(PROP_DRM_HEADERS) ? drm.getMap(PROP_DRM_HEADERS) : null;
|
||||
if (drmType != null && drmLicenseServer != null && Util.getDrmUuid(drmType) != null) {
|
||||
UUID drmUUID = Util.getDrmUuid(drmType);
|
||||
videoView.setDrmType(drmUUID);
|
||||
videoView.setDrmLicenseUrl(drmLicenseServer);
|
||||
if (drmHeaders != null) {
|
||||
ArrayList<String> drmKeyRequestPropertiesList = new ArrayList<>();
|
||||
ReadableMapKeySetIterator itr = drmHeaders.keySetIterator();
|
||||
while (itr.hasNextKey()) {
|
||||
String key = itr.nextKey();
|
||||
drmKeyRequestPropertiesList.add(key);
|
||||
drmKeyRequestPropertiesList.add(drmHeaders.getString(key));
|
||||
}
|
||||
videoView.setDrmLicenseHeader(drmKeyRequestPropertiesList.toArray(new String[0]));
|
||||
}
|
||||
videoView.setUseTextureView(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_SRC)
|
||||
public void setSrc(final ReactExoplayerView videoView, @Nullable ReadableMap src) {
|
||||
Context context = videoView.getContext().getApplicationContext();
|
||||
String uriString = src.hasKey(PROP_SRC_URI) ? src.getString(PROP_SRC_URI) : null;
|
||||
String extension = src.hasKey(PROP_SRC_TYPE) ? src.getString(PROP_SRC_TYPE) : null;
|
||||
Map<String, String> headers = src.hasKey(PROP_SRC_HEADERS) ? toStringMap(src.getMap(PROP_SRC_HEADERS)) : null;
|
||||
|
||||
if (TextUtils.isEmpty(uriString)) {
|
||||
videoView.clearSrc();
|
||||
return;
|
||||
}
|
||||
|
||||
if (startsWithValidScheme(uriString)) {
|
||||
Uri srcUri = Uri.parse(uriString);
|
||||
|
||||
if (srcUri != null) {
|
||||
videoView.setSrc(srcUri, extension, headers);
|
||||
}
|
||||
} else {
|
||||
int identifier = context.getResources().getIdentifier(
|
||||
uriString,
|
||||
"drawable",
|
||||
context.getPackageName()
|
||||
);
|
||||
if (identifier == 0) {
|
||||
identifier = context.getResources().getIdentifier(
|
||||
uriString,
|
||||
"raw",
|
||||
context.getPackageName()
|
||||
);
|
||||
}
|
||||
if (identifier > 0) {
|
||||
Uri srcUri = RawResourceDataSource.buildRawResourceUri(identifier);
|
||||
if (srcUri != null) {
|
||||
videoView.setRawSrc(srcUri, extension);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_RESIZE_MODE)
|
||||
public void setResizeMode(final ReactExoplayerView videoView, final String resizeModeOrdinalString) {
|
||||
videoView.setResizeModeModifier(convertToIntDef(resizeModeOrdinalString));
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_REPEAT, defaultBoolean = false)
|
||||
public void setRepeat(final ReactExoplayerView videoView, final boolean repeat) {
|
||||
videoView.setRepeatModifier(repeat);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_PREVENTS_DISPLAY_SLEEP_DURING_VIDEO_PLAYBACK, defaultBoolean = false)
|
||||
public void setPreventsDisplaySleepDuringVideoPlayback(final ReactExoplayerView videoView, final boolean preventsSleep) {
|
||||
videoView.setPreventsDisplaySleepDuringVideoPlayback(preventsSleep);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_SELECTED_VIDEO_TRACK)
|
||||
public void setSelectedVideoTrack(final ReactExoplayerView videoView,
|
||||
@Nullable ReadableMap selectedVideoTrack) {
|
||||
String typeString = null;
|
||||
Dynamic value = null;
|
||||
if (selectedVideoTrack != null) {
|
||||
typeString = selectedVideoTrack.hasKey(PROP_SELECTED_VIDEO_TRACK_TYPE)
|
||||
? selectedVideoTrack.getString(PROP_SELECTED_VIDEO_TRACK_TYPE) : null;
|
||||
value = selectedVideoTrack.hasKey(PROP_SELECTED_VIDEO_TRACK_VALUE)
|
||||
? selectedVideoTrack.getDynamic(PROP_SELECTED_VIDEO_TRACK_VALUE) : null;
|
||||
}
|
||||
videoView.setSelectedVideoTrack(typeString, value);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_SELECTED_AUDIO_TRACK)
|
||||
public void setSelectedAudioTrack(final ReactExoplayerView videoView,
|
||||
@Nullable ReadableMap selectedAudioTrack) {
|
||||
String typeString = null;
|
||||
Dynamic value = null;
|
||||
if (selectedAudioTrack != null) {
|
||||
typeString = selectedAudioTrack.hasKey(PROP_SELECTED_AUDIO_TRACK_TYPE)
|
||||
? selectedAudioTrack.getString(PROP_SELECTED_AUDIO_TRACK_TYPE) : null;
|
||||
value = selectedAudioTrack.hasKey(PROP_SELECTED_AUDIO_TRACK_VALUE)
|
||||
? selectedAudioTrack.getDynamic(PROP_SELECTED_AUDIO_TRACK_VALUE) : null;
|
||||
}
|
||||
videoView.setSelectedAudioTrack(typeString, value);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_SELECTED_TEXT_TRACK)
|
||||
public void setSelectedTextTrack(final ReactExoplayerView videoView,
|
||||
@Nullable ReadableMap selectedTextTrack) {
|
||||
String typeString = null;
|
||||
Dynamic value = null;
|
||||
if (selectedTextTrack != null) {
|
||||
typeString = selectedTextTrack.hasKey(PROP_SELECTED_TEXT_TRACK_TYPE)
|
||||
? selectedTextTrack.getString(PROP_SELECTED_TEXT_TRACK_TYPE) : null;
|
||||
value = selectedTextTrack.hasKey(PROP_SELECTED_TEXT_TRACK_VALUE)
|
||||
? selectedTextTrack.getDynamic(PROP_SELECTED_TEXT_TRACK_VALUE) : null;
|
||||
}
|
||||
videoView.setSelectedTextTrack(typeString, value);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_TEXT_TRACKS)
|
||||
public void setPropTextTracks(final ReactExoplayerView videoView,
|
||||
@Nullable ReadableArray textTracks) {
|
||||
videoView.setTextTracks(textTracks);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_PAUSED, defaultBoolean = false)
|
||||
public void setPaused(final ReactExoplayerView videoView, final boolean paused) {
|
||||
videoView.setPausedModifier(paused);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_MUTED, defaultBoolean = false)
|
||||
public void setMuted(final ReactExoplayerView videoView, final boolean muted) {
|
||||
videoView.setMutedModifier(muted);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_VOLUME, defaultFloat = 1.0f)
|
||||
public void setVolume(final ReactExoplayerView videoView, final float volume) {
|
||||
videoView.setVolumeModifier(volume);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_PROGRESS_UPDATE_INTERVAL, defaultFloat = 250.0f)
|
||||
public void setProgressUpdateInterval(final ReactExoplayerView videoView, final float progressUpdateInterval) {
|
||||
videoView.setProgressUpdateInterval(progressUpdateInterval);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_REPORT_BANDWIDTH, defaultBoolean = false)
|
||||
public void setReportBandwidth(final ReactExoplayerView videoView, final boolean reportBandwidth) {
|
||||
videoView.setReportBandwidth(reportBandwidth);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_SEEK)
|
||||
public void setSeek(final ReactExoplayerView videoView, final float seek) {
|
||||
videoView.seekTo(Math.round(seek * 1000f));
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_RATE)
|
||||
public void setRate(final ReactExoplayerView videoView, final float rate) {
|
||||
videoView.setRateModifier(rate);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_MAXIMUM_BIT_RATE)
|
||||
public void setMaxBitRate(final ReactExoplayerView videoView, final int maxBitRate) {
|
||||
videoView.setMaxBitRateModifier(maxBitRate);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_MIN_LOAD_RETRY_COUNT)
|
||||
public void minLoadRetryCount(final ReactExoplayerView videoView, final int minLoadRetryCount) {
|
||||
videoView.setMinLoadRetryCountModifier(minLoadRetryCount);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_PLAY_IN_BACKGROUND, defaultBoolean = false)
|
||||
public void setPlayInBackground(final ReactExoplayerView videoView, final boolean playInBackground) {
|
||||
videoView.setPlayInBackground(playInBackground);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_DISABLE_FOCUS, defaultBoolean = false)
|
||||
public void setDisableFocus(final ReactExoplayerView videoView, final boolean disableFocus) {
|
||||
videoView.setDisableFocus(disableFocus);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_BACK_BUFFER_DURATION_MS, defaultInt = 0)
|
||||
public void setBackBufferDurationMs(final ReactExoplayerView videoView, final int backBufferDurationMs) {
|
||||
videoView.setBackBufferDurationMs(backBufferDurationMs);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_CONTENT_START_TIME, defaultInt = 0)
|
||||
public void setContentStartTime(final ReactExoplayerView videoView, final int contentStartTime) {
|
||||
videoView.setContentStartTime(contentStartTime);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_DISABLE_BUFFERING, defaultBoolean = false)
|
||||
public void setDisableBuffering(final ReactExoplayerView videoView, final boolean disableBuffering) {
|
||||
videoView.setDisableBuffering(disableBuffering);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_DISABLE_DISCONNECT_ERROR, defaultBoolean = false)
|
||||
public void setDisableDisconnectError(final ReactExoplayerView videoView, final boolean disableDisconnectError) {
|
||||
videoView.setDisableDisconnectError(disableDisconnectError);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_FULLSCREEN, defaultBoolean = false)
|
||||
public void setFullscreen(final ReactExoplayerView videoView, final boolean fullscreen) {
|
||||
videoView.setFullscreen(fullscreen);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_USE_TEXTURE_VIEW, defaultBoolean = true)
|
||||
public void setUseTextureView(final ReactExoplayerView videoView, final boolean useTextureView) {
|
||||
videoView.setUseTextureView(useTextureView);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_SECURE_VIEW, defaultBoolean = true)
|
||||
public void useSecureView(final ReactExoplayerView videoView, final boolean useSecureView) {
|
||||
videoView.useSecureView(useSecureView);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_HIDE_SHUTTER_VIEW, defaultBoolean = false)
|
||||
public void setHideShutterView(final ReactExoplayerView videoView, final boolean hideShutterView) {
|
||||
videoView.setHideShutterView(hideShutterView);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_CONTROLS, defaultBoolean = false)
|
||||
public void setControls(final ReactExoplayerView videoView, final boolean controls) {
|
||||
videoView.setControls(controls);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_BUFFER_CONFIG)
|
||||
public void setBufferConfig(final ReactExoplayerView videoView, @Nullable ReadableMap bufferConfig) {
|
||||
int minBufferMs = DefaultLoadControl.DEFAULT_MIN_BUFFER_MS;
|
||||
int maxBufferMs = DefaultLoadControl.DEFAULT_MAX_BUFFER_MS;
|
||||
int bufferForPlaybackMs = DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS;
|
||||
int bufferForPlaybackAfterRebufferMs = DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS;
|
||||
double maxHeapAllocationPercent = ReactExoplayerView.DEFAULT_MAX_HEAP_ALLOCATION_PERCENT;
|
||||
double minBackBufferMemoryReservePercent = ReactExoplayerView.DEFAULT_MIN_BACK_BUFFER_MEMORY_RESERVE;
|
||||
double minBufferMemoryReservePercent = ReactExoplayerView.DEFAULT_MIN_BUFFER_MEMORY_RESERVE;
|
||||
|
||||
if (bufferConfig != null) {
|
||||
minBufferMs = bufferConfig.hasKey(PROP_BUFFER_CONFIG_MIN_BUFFER_MS)
|
||||
? bufferConfig.getInt(PROP_BUFFER_CONFIG_MIN_BUFFER_MS) : minBufferMs;
|
||||
maxBufferMs = bufferConfig.hasKey(PROP_BUFFER_CONFIG_MAX_BUFFER_MS)
|
||||
? bufferConfig.getInt(PROP_BUFFER_CONFIG_MAX_BUFFER_MS) : maxBufferMs;
|
||||
bufferForPlaybackMs = bufferConfig.hasKey(PROP_BUFFER_CONFIG_BUFFER_FOR_PLAYBACK_MS)
|
||||
? bufferConfig.getInt(PROP_BUFFER_CONFIG_BUFFER_FOR_PLAYBACK_MS) : bufferForPlaybackMs;
|
||||
bufferForPlaybackAfterRebufferMs = bufferConfig.hasKey(PROP_BUFFER_CONFIG_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS)
|
||||
? bufferConfig.getInt(PROP_BUFFER_CONFIG_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS) : bufferForPlaybackAfterRebufferMs;
|
||||
maxHeapAllocationPercent = bufferConfig.hasKey(PROP_BUFFER_CONFIG_MAX_HEAP_ALLOCATION_PERCENT)
|
||||
? bufferConfig.getDouble(PROP_BUFFER_CONFIG_MAX_HEAP_ALLOCATION_PERCENT) : maxHeapAllocationPercent;
|
||||
minBackBufferMemoryReservePercent = bufferConfig.hasKey(PROP_BUFFER_CONFIG_MIN_BACK_BUFFER_MEMORY_RESERVE_PERCENT)
|
||||
? bufferConfig.getDouble(PROP_BUFFER_CONFIG_MIN_BACK_BUFFER_MEMORY_RESERVE_PERCENT) : minBackBufferMemoryReservePercent;
|
||||
minBufferMemoryReservePercent = bufferConfig.hasKey(PROP_BUFFER_CONFIG_MIN_BUFFER_MEMORY_RESERVE_PERCENT)
|
||||
? bufferConfig.getDouble(PROP_BUFFER_CONFIG_MIN_BUFFER_MEMORY_RESERVE_PERCENT) : minBufferMemoryReservePercent;
|
||||
videoView.setBufferConfig(minBufferMs, maxBufferMs, bufferForPlaybackMs, bufferForPlaybackAfterRebufferMs, maxHeapAllocationPercent, minBackBufferMemoryReservePercent, minBufferMemoryReservePercent);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean startsWithValidScheme(String uriString) {
|
||||
return uriString.startsWith("http://")
|
||||
|| uriString.startsWith("https://")
|
||||
|| uriString.startsWith("content://")
|
||||
|| uriString.startsWith("file://")
|
||||
|| uriString.startsWith("asset://");
|
||||
}
|
||||
|
||||
private @ResizeMode.Mode int convertToIntDef(String resizeModeOrdinalString) {
|
||||
if (!TextUtils.isEmpty(resizeModeOrdinalString)) {
|
||||
int resizeModeOrdinal = Integer.parseInt(resizeModeOrdinalString);
|
||||
return ResizeMode.toResizeMode(resizeModeOrdinal);
|
||||
}
|
||||
return ResizeMode.RESIZE_MODE_FIT;
|
||||
}
|
||||
|
||||
/**
|
||||
* toStringMap converts a {@link ReadableMap} into a HashMap.
|
||||
*
|
||||
* @param readableMap The ReadableMap to be conveted.
|
||||
* @return A HashMap containing the data that was in the ReadableMap.
|
||||
* @see 'Adapted from https://github.com/artemyarulin/react-native-eval/blob/master/android/src/main/java/com/evaluator/react/ConversionUtil.java'
|
||||
*/
|
||||
public static Map<String, String> toStringMap(@Nullable ReadableMap readableMap) {
|
||||
if (readableMap == null)
|
||||
return null;
|
||||
|
||||
com.facebook.react.bridge.ReadableMapKeySetIterator iterator = readableMap.keySetIterator();
|
||||
if (!iterator.hasNextKey())
|
||||
return null;
|
||||
|
||||
Map<String, String> result = new HashMap<>();
|
||||
while (iterator.hasNextKey()) {
|
||||
String key = iterator.nextKey();
|
||||
result.put(key, readableMap.getString(key));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
@@ -0,0 +1,63 @@
|
||||
package com.brentvatne.exoplayer;
|
||||
|
||||
import androidx.annotation.IntDef;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
|
||||
import static java.lang.annotation.RetentionPolicy.SOURCE;
|
||||
|
||||
class ResizeMode {
|
||||
|
||||
/**
|
||||
* Either the width or height is decreased to obtain the desired aspect ratio.
|
||||
*/
|
||||
static final int RESIZE_MODE_FIT = 0;
|
||||
/**
|
||||
* The width is fixed and the height is increased or decreased to obtain the desired aspect ratio.
|
||||
*/
|
||||
static final int RESIZE_MODE_FIXED_WIDTH = 1;
|
||||
/**
|
||||
* The height is fixed and the width is increased or decreased to obtain the desired aspect ratio.
|
||||
*/
|
||||
static final int RESIZE_MODE_FIXED_HEIGHT = 2;
|
||||
/**
|
||||
* The height and the width is increased or decreased to fit the size of the view.
|
||||
*/
|
||||
static final int RESIZE_MODE_FILL = 3;
|
||||
/**
|
||||
* Keeps the aspect ratio but takes up the view's size.
|
||||
*/
|
||||
static final int RESIZE_MODE_CENTER_CROP = 4;
|
||||
|
||||
@Retention(SOURCE)
|
||||
@IntDef({
|
||||
RESIZE_MODE_FIT,
|
||||
RESIZE_MODE_FIXED_WIDTH,
|
||||
RESIZE_MODE_FIXED_HEIGHT,
|
||||
RESIZE_MODE_FILL,
|
||||
RESIZE_MODE_CENTER_CROP
|
||||
})
|
||||
public @interface Mode {
|
||||
}
|
||||
|
||||
@ResizeMode.Mode static int toResizeMode(int ordinal) {
|
||||
switch (ordinal) {
|
||||
case ResizeMode.RESIZE_MODE_FIXED_WIDTH:
|
||||
return ResizeMode.RESIZE_MODE_FIXED_WIDTH;
|
||||
|
||||
case ResizeMode.RESIZE_MODE_FIXED_HEIGHT:
|
||||
return ResizeMode.RESIZE_MODE_FIXED_HEIGHT;
|
||||
|
||||
case ResizeMode.RESIZE_MODE_FILL:
|
||||
return ResizeMode.RESIZE_MODE_FILL;
|
||||
|
||||
case ResizeMode.RESIZE_MODE_CENTER_CROP:
|
||||
return ResizeMode.RESIZE_MODE_CENTER_CROP;
|
||||
|
||||
case ResizeMode.RESIZE_MODE_FIT:
|
||||
default:
|
||||
return ResizeMode.RESIZE_MODE_FIT;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,336 @@
|
||||
package com.brentvatne.exoplayer;
|
||||
|
||||
import androidx.annotation.StringDef;
|
||||
import android.view.View;
|
||||
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.ReactContext;
|
||||
import com.facebook.react.bridge.WritableArray;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.uimanager.events.RCTEventEmitter;
|
||||
import com.google.android.exoplayer2.metadata.Metadata;
|
||||
import com.google.android.exoplayer2.metadata.emsg.EventMessage;
|
||||
import com.google.android.exoplayer2.metadata.id3.Id3Frame;
|
||||
import com.google.android.exoplayer2.metadata.id3.TextInformationFrame;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.io.StringWriter;
|
||||
import java.io.PrintWriter;
|
||||
|
||||
class VideoEventEmitter {
|
||||
|
||||
private final RCTEventEmitter eventEmitter;
|
||||
|
||||
private int viewId = View.NO_ID;
|
||||
|
||||
VideoEventEmitter(ReactContext reactContext) {
|
||||
this.eventEmitter = reactContext.getJSModule(RCTEventEmitter.class);
|
||||
}
|
||||
|
||||
private static final String EVENT_LOAD_START = "onVideoLoadStart";
|
||||
private static final String EVENT_LOAD = "onVideoLoad";
|
||||
private static final String EVENT_ERROR = "onVideoError";
|
||||
private static final String EVENT_PROGRESS = "onVideoProgress";
|
||||
private static final String EVENT_BANDWIDTH = "onVideoBandwidthUpdate";
|
||||
private static final String EVENT_SEEK = "onVideoSeek";
|
||||
private static final String EVENT_END = "onVideoEnd";
|
||||
private static final String EVENT_FULLSCREEN_WILL_PRESENT = "onVideoFullscreenPlayerWillPresent";
|
||||
private static final String EVENT_FULLSCREEN_DID_PRESENT = "onVideoFullscreenPlayerDidPresent";
|
||||
private static final String EVENT_FULLSCREEN_WILL_DISMISS = "onVideoFullscreenPlayerWillDismiss";
|
||||
private static final String EVENT_FULLSCREEN_DID_DISMISS = "onVideoFullscreenPlayerDidDismiss";
|
||||
|
||||
private static final String EVENT_STALLED = "onPlaybackStalled";
|
||||
private static final String EVENT_RESUME = "onPlaybackResume";
|
||||
private static final String EVENT_READY = "onReadyForDisplay";
|
||||
private static final String EVENT_BUFFER = "onVideoBuffer";
|
||||
private static final String EVENT_PLAYBACK_STATE_CHANGED = "onVideoPlaybackStateChanged";
|
||||
private static final String EVENT_IDLE = "onVideoIdle";
|
||||
private static final String EVENT_TIMED_METADATA = "onTimedMetadata";
|
||||
private static final String EVENT_AUDIO_BECOMING_NOISY = "onVideoAudioBecomingNoisy";
|
||||
private static final String EVENT_AUDIO_FOCUS_CHANGE = "onAudioFocusChanged";
|
||||
private static final String EVENT_PLAYBACK_RATE_CHANGE = "onPlaybackRateChange";
|
||||
|
||||
static final String[] Events = {
|
||||
EVENT_LOAD_START,
|
||||
EVENT_LOAD,
|
||||
EVENT_ERROR,
|
||||
EVENT_PROGRESS,
|
||||
EVENT_SEEK,
|
||||
EVENT_END,
|
||||
EVENT_FULLSCREEN_WILL_PRESENT,
|
||||
EVENT_FULLSCREEN_DID_PRESENT,
|
||||
EVENT_FULLSCREEN_WILL_DISMISS,
|
||||
EVENT_FULLSCREEN_DID_DISMISS,
|
||||
EVENT_STALLED,
|
||||
EVENT_RESUME,
|
||||
EVENT_READY,
|
||||
EVENT_BUFFER,
|
||||
EVENT_PLAYBACK_STATE_CHANGED,
|
||||
EVENT_IDLE,
|
||||
EVENT_TIMED_METADATA,
|
||||
EVENT_AUDIO_BECOMING_NOISY,
|
||||
EVENT_AUDIO_FOCUS_CHANGE,
|
||||
EVENT_PLAYBACK_RATE_CHANGE,
|
||||
EVENT_BANDWIDTH,
|
||||
};
|
||||
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@StringDef({
|
||||
EVENT_LOAD_START,
|
||||
EVENT_LOAD,
|
||||
EVENT_ERROR,
|
||||
EVENT_PROGRESS,
|
||||
EVENT_SEEK,
|
||||
EVENT_END,
|
||||
EVENT_FULLSCREEN_WILL_PRESENT,
|
||||
EVENT_FULLSCREEN_DID_PRESENT,
|
||||
EVENT_FULLSCREEN_WILL_DISMISS,
|
||||
EVENT_FULLSCREEN_DID_DISMISS,
|
||||
EVENT_STALLED,
|
||||
EVENT_RESUME,
|
||||
EVENT_READY,
|
||||
EVENT_BUFFER,
|
||||
EVENT_PLAYBACK_STATE_CHANGED,
|
||||
EVENT_IDLE,
|
||||
EVENT_TIMED_METADATA,
|
||||
EVENT_AUDIO_BECOMING_NOISY,
|
||||
EVENT_AUDIO_FOCUS_CHANGE,
|
||||
EVENT_PLAYBACK_RATE_CHANGE,
|
||||
EVENT_BANDWIDTH,
|
||||
})
|
||||
@interface VideoEvents {
|
||||
}
|
||||
|
||||
private static final String EVENT_PROP_FAST_FORWARD = "canPlayFastForward";
|
||||
private static final String EVENT_PROP_SLOW_FORWARD = "canPlaySlowForward";
|
||||
private static final String EVENT_PROP_SLOW_REVERSE = "canPlaySlowReverse";
|
||||
private static final String EVENT_PROP_REVERSE = "canPlayReverse";
|
||||
private static final String EVENT_PROP_STEP_FORWARD = "canStepForward";
|
||||
private static final String EVENT_PROP_STEP_BACKWARD = "canStepBackward";
|
||||
|
||||
private static final String EVENT_PROP_BUFFER_START = "bufferStart";
|
||||
private static final String EVENT_PROP_BUFFER_END = "bufferEnd";
|
||||
private static final String EVENT_PROP_DURATION = "duration";
|
||||
private static final String EVENT_PROP_PLAYABLE_DURATION = "playableDuration";
|
||||
private static final String EVENT_PROP_SEEKABLE_DURATION = "seekableDuration";
|
||||
private static final String EVENT_PROP_CURRENT_TIME = "currentTime";
|
||||
private static final String EVENT_PROP_CURRENT_PLAYBACK_TIME = "currentPlaybackTime";
|
||||
private static final String EVENT_PROP_SEEK_TIME = "seekTime";
|
||||
private static final String EVENT_PROP_NATURAL_SIZE = "naturalSize";
|
||||
private static final String EVENT_PROP_TRACK_ID = "trackId";
|
||||
private static final String EVENT_PROP_WIDTH = "width";
|
||||
private static final String EVENT_PROP_HEIGHT = "height";
|
||||
private static final String EVENT_PROP_ORIENTATION = "orientation";
|
||||
private static final String EVENT_PROP_VIDEO_TRACKS = "videoTracks";
|
||||
private static final String EVENT_PROP_AUDIO_TRACKS = "audioTracks";
|
||||
private static final String EVENT_PROP_TEXT_TRACKS = "textTracks";
|
||||
private static final String EVENT_PROP_HAS_AUDIO_FOCUS = "hasAudioFocus";
|
||||
private static final String EVENT_PROP_IS_BUFFERING = "isBuffering";
|
||||
private static final String EVENT_PROP_PLAYBACK_RATE = "playbackRate";
|
||||
|
||||
private static final String EVENT_PROP_ERROR = "error";
|
||||
private static final String EVENT_PROP_ERROR_STRING = "errorString";
|
||||
private static final String EVENT_PROP_ERROR_EXCEPTION = "errorException";
|
||||
private static final String EVENT_PROP_ERROR_TRACE = "errorStackTrace";
|
||||
private static final String EVENT_PROP_ERROR_CODE = "errorCode";
|
||||
|
||||
private static final String EVENT_PROP_TIMED_METADATA = "metadata";
|
||||
|
||||
private static final String EVENT_PROP_BITRATE = "bitrate";
|
||||
|
||||
private static final String EVENT_PROP_IS_PLAYING = "isPlaying";
|
||||
|
||||
void setViewId(int viewId) {
|
||||
this.viewId = viewId;
|
||||
}
|
||||
|
||||
void loadStart() {
|
||||
receiveEvent(EVENT_LOAD_START, null);
|
||||
}
|
||||
|
||||
void load(double duration, double currentPosition, int videoWidth, int videoHeight,
|
||||
WritableArray audioTracks, WritableArray textTracks, WritableArray videoTracks, String trackId) {
|
||||
WritableMap event = Arguments.createMap();
|
||||
event.putDouble(EVENT_PROP_DURATION, duration / 1000D);
|
||||
event.putDouble(EVENT_PROP_CURRENT_TIME, currentPosition / 1000D);
|
||||
|
||||
WritableMap naturalSize = Arguments.createMap();
|
||||
naturalSize.putInt(EVENT_PROP_WIDTH, videoWidth);
|
||||
naturalSize.putInt(EVENT_PROP_HEIGHT, videoHeight);
|
||||
if (videoWidth > videoHeight) {
|
||||
naturalSize.putString(EVENT_PROP_ORIENTATION, "landscape");
|
||||
} else {
|
||||
naturalSize.putString(EVENT_PROP_ORIENTATION, "portrait");
|
||||
}
|
||||
event.putMap(EVENT_PROP_NATURAL_SIZE, naturalSize);
|
||||
event.putString(EVENT_PROP_TRACK_ID, trackId);
|
||||
event.putArray(EVENT_PROP_VIDEO_TRACKS, videoTracks);
|
||||
event.putArray(EVENT_PROP_AUDIO_TRACKS, audioTracks);
|
||||
event.putArray(EVENT_PROP_TEXT_TRACKS, textTracks);
|
||||
|
||||
// TODO: Actually check if you can.
|
||||
event.putBoolean(EVENT_PROP_FAST_FORWARD, true);
|
||||
event.putBoolean(EVENT_PROP_SLOW_FORWARD, true);
|
||||
event.putBoolean(EVENT_PROP_SLOW_REVERSE, true);
|
||||
event.putBoolean(EVENT_PROP_REVERSE, true);
|
||||
event.putBoolean(EVENT_PROP_FAST_FORWARD, true);
|
||||
event.putBoolean(EVENT_PROP_STEP_BACKWARD, true);
|
||||
event.putBoolean(EVENT_PROP_STEP_FORWARD, true);
|
||||
|
||||
receiveEvent(EVENT_LOAD, event);
|
||||
}
|
||||
|
||||
void progressChanged(double currentPosition, double bufferedDuration, double seekableDuration, double currentPlaybackTime) {
|
||||
WritableMap event = Arguments.createMap();
|
||||
event.putDouble(EVENT_PROP_CURRENT_TIME, currentPosition / 1000D);
|
||||
event.putDouble(EVENT_PROP_PLAYABLE_DURATION, bufferedDuration / 1000D);
|
||||
event.putDouble(EVENT_PROP_SEEKABLE_DURATION, seekableDuration / 1000D);
|
||||
event.putDouble(EVENT_PROP_CURRENT_PLAYBACK_TIME, currentPlaybackTime);
|
||||
receiveEvent(EVENT_PROGRESS, event);
|
||||
}
|
||||
|
||||
void bandwidthReport(double bitRateEstimate, int height, int width, String id) {
|
||||
WritableMap event = Arguments.createMap();
|
||||
event.putDouble(EVENT_PROP_BITRATE, bitRateEstimate);
|
||||
event.putInt(EVENT_PROP_WIDTH, width);
|
||||
event.putInt(EVENT_PROP_HEIGHT, height);
|
||||
event.putString(EVENT_PROP_TRACK_ID, id);
|
||||
receiveEvent(EVENT_BANDWIDTH, event);
|
||||
}
|
||||
|
||||
void seek(long currentPosition, long seekTime) {
|
||||
WritableMap event = Arguments.createMap();
|
||||
event.putDouble(EVENT_PROP_CURRENT_TIME, currentPosition / 1000D);
|
||||
event.putDouble(EVENT_PROP_SEEK_TIME, seekTime / 1000D);
|
||||
receiveEvent(EVENT_SEEK, event);
|
||||
}
|
||||
|
||||
void ready() {
|
||||
receiveEvent(EVENT_READY, null);
|
||||
}
|
||||
|
||||
void buffering(boolean isBuffering) {
|
||||
WritableMap map = Arguments.createMap();
|
||||
map.putBoolean(EVENT_PROP_IS_BUFFERING, isBuffering);
|
||||
receiveEvent(EVENT_BUFFER, map);
|
||||
}
|
||||
|
||||
void playbackStateChanged(boolean isPlaying) {
|
||||
WritableMap map = Arguments.createMap();
|
||||
map.putBoolean(EVENT_PROP_IS_PLAYING, isPlaying);
|
||||
receiveEvent(EVENT_PLAYBACK_STATE_CHANGED, map);
|
||||
}
|
||||
|
||||
void idle() {
|
||||
receiveEvent(EVENT_IDLE, null);
|
||||
}
|
||||
|
||||
void end() {
|
||||
receiveEvent(EVENT_END, null);
|
||||
}
|
||||
|
||||
void fullscreenWillPresent() {
|
||||
receiveEvent(EVENT_FULLSCREEN_WILL_PRESENT, null);
|
||||
}
|
||||
|
||||
void fullscreenDidPresent() {
|
||||
receiveEvent(EVENT_FULLSCREEN_DID_PRESENT, null);
|
||||
}
|
||||
|
||||
void fullscreenWillDismiss() {
|
||||
receiveEvent(EVENT_FULLSCREEN_WILL_DISMISS, null);
|
||||
}
|
||||
|
||||
void fullscreenDidDismiss() {
|
||||
receiveEvent(EVENT_FULLSCREEN_DID_DISMISS, null);
|
||||
}
|
||||
|
||||
void error(String errorString, Exception exception) {
|
||||
_error(errorString, exception, "0001");
|
||||
}
|
||||
|
||||
void error(String errorString, Exception exception, String errorCode) {
|
||||
_error(errorString, exception, errorCode);
|
||||
}
|
||||
|
||||
void _error(String errorString, Exception exception, String errorCode) {
|
||||
// Prepare stack trace
|
||||
StringWriter sw = new StringWriter();
|
||||
PrintWriter pw = new PrintWriter(sw);
|
||||
exception.printStackTrace(pw);
|
||||
String stackTrace = sw.toString();
|
||||
|
||||
WritableMap error = Arguments.createMap();
|
||||
error.putString(EVENT_PROP_ERROR_STRING, errorString);
|
||||
error.putString(EVENT_PROP_ERROR_EXCEPTION, exception.toString());
|
||||
error.putString(EVENT_PROP_ERROR_CODE, errorCode);
|
||||
error.putString(EVENT_PROP_ERROR_TRACE, stackTrace);
|
||||
WritableMap event = Arguments.createMap();
|
||||
event.putMap(EVENT_PROP_ERROR, error);
|
||||
receiveEvent(EVENT_ERROR, event);
|
||||
}
|
||||
|
||||
void playbackRateChange(float rate) {
|
||||
WritableMap map = Arguments.createMap();
|
||||
map.putDouble(EVENT_PROP_PLAYBACK_RATE, (double)rate);
|
||||
receiveEvent(EVENT_PLAYBACK_RATE_CHANGE, map);
|
||||
}
|
||||
|
||||
void timedMetadata(Metadata metadata) {
|
||||
WritableArray metadataArray = Arguments.createArray();
|
||||
|
||||
for (int i = 0; i < metadata.length(); i++) {
|
||||
|
||||
Metadata.Entry entry = metadata.get(i);
|
||||
|
||||
if (entry instanceof Id3Frame) {
|
||||
|
||||
Id3Frame frame = (Id3Frame) entry;
|
||||
|
||||
String value = "";
|
||||
|
||||
if (frame instanceof TextInformationFrame) {
|
||||
TextInformationFrame txxxFrame = (TextInformationFrame) frame;
|
||||
value = txxxFrame.value;
|
||||
}
|
||||
|
||||
String identifier = frame.id;
|
||||
|
||||
WritableMap map = Arguments.createMap();
|
||||
map.putString("identifier", identifier);
|
||||
map.putString("value", value);
|
||||
|
||||
metadataArray.pushMap(map);
|
||||
|
||||
} else if (entry instanceof EventMessage) {
|
||||
|
||||
EventMessage eventMessage = (EventMessage) entry;
|
||||
|
||||
WritableMap map = Arguments.createMap();
|
||||
map.putString("identifier", eventMessage.schemeIdUri);
|
||||
map.putString("value", eventMessage.value);
|
||||
metadataArray.pushMap(map);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
WritableMap event = Arguments.createMap();
|
||||
event.putArray(EVENT_PROP_TIMED_METADATA, metadataArray);
|
||||
receiveEvent(EVENT_TIMED_METADATA, event);
|
||||
}
|
||||
|
||||
void audioFocusChanged(boolean hasFocus) {
|
||||
WritableMap map = Arguments.createMap();
|
||||
map.putBoolean(EVENT_PROP_HAS_AUDIO_FOCUS, hasFocus);
|
||||
receiveEvent(EVENT_AUDIO_FOCUS_CHANGE, map);
|
||||
}
|
||||
|
||||
void audioBecomingNoisy() {
|
||||
receiveEvent(EVENT_AUDIO_BECOMING_NOISY, null);
|
||||
}
|
||||
|
||||
private void receiveEvent(@VideoEvents String type, WritableMap event) {
|
||||
eventEmitter.receiveEvent(viewId, type, event);
|
||||
}
|
||||
}
|
@@ -1,18 +1,28 @@
|
||||
package com.brentvatne.react;
|
||||
|
||||
import android.app.Activity;
|
||||
import com.brentvatne.exoplayer.DefaultReactExoplayerConfig;
|
||||
import com.brentvatne.exoplayer.ReactExoplayerConfig;
|
||||
import com.brentvatne.exoplayer.ReactExoplayerViewManager;
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.bridge.JavaScriptModule;
|
||||
import com.facebook.react.bridge.NativeModule;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.uimanager.ViewManager;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class ReactVideoPackage implements ReactPackage {
|
||||
|
||||
private ReactExoplayerConfig config;
|
||||
|
||||
public ReactVideoPackage() {
|
||||
}
|
||||
|
||||
public ReactVideoPackage(ReactExoplayerConfig config) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
|
||||
return Collections.emptyList();
|
||||
@@ -23,8 +33,12 @@ public class ReactVideoPackage implements ReactPackage {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
|
||||
return Arrays.<ViewManager>asList(new ReactVideoViewManager());
|
||||
if (config == null) {
|
||||
config = new DefaultReactExoplayerConfig(reactContext);
|
||||
}
|
||||
return Collections.singletonList(new ReactExoplayerViewManager(config));
|
||||
}
|
||||
}
|
||||
|
@@ -1,796 +0,0 @@
|
||||
package com.brentvatne.react;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.Activity;
|
||||
import android.content.res.AssetFileDescriptor;
|
||||
import android.graphics.Matrix;
|
||||
import android.media.MediaPlayer;
|
||||
import android.media.TimedMetaData;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.util.Log;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.WindowManager;
|
||||
import android.view.View;
|
||||
import android.view.Window;
|
||||
import android.webkit.CookieManager;
|
||||
import android.widget.MediaController;
|
||||
|
||||
import com.android.vending.expansion.zipfile.APKExpansionSupport;
|
||||
import com.android.vending.expansion.zipfile.ZipResourceFile;
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.LifecycleEventListener;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.bridge.WritableArray;
|
||||
import com.facebook.react.bridge.WritableNativeArray;
|
||||
import com.facebook.react.uimanager.ThemedReactContext;
|
||||
import com.facebook.react.uimanager.events.RCTEventEmitter;
|
||||
import com.yqritc.scalablevideoview.ScalableType;
|
||||
import com.yqritc.scalablevideoview.ScalableVideoView;
|
||||
import com.yqritc.scalablevideoview.ScaleManager;
|
||||
import com.yqritc.scalablevideoview.Size;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.lang.Math;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
@SuppressLint("ViewConstructor")
|
||||
public class ReactVideoView extends ScalableVideoView implements
|
||||
MediaPlayer.OnPreparedListener,
|
||||
MediaPlayer.OnErrorListener,
|
||||
MediaPlayer.OnBufferingUpdateListener,
|
||||
MediaPlayer.OnSeekCompleteListener,
|
||||
MediaPlayer.OnCompletionListener,
|
||||
MediaPlayer.OnInfoListener,
|
||||
LifecycleEventListener,
|
||||
MediaController.MediaPlayerControl {
|
||||
|
||||
public enum Events {
|
||||
EVENT_LOAD_START("onVideoLoadStart"),
|
||||
EVENT_LOAD("onVideoLoad"),
|
||||
EVENT_ERROR("onVideoError"),
|
||||
EVENT_PROGRESS("onVideoProgress"),
|
||||
EVENT_TIMED_METADATA("onTimedMetadata"),
|
||||
EVENT_SEEK("onVideoSeek"),
|
||||
EVENT_END("onVideoEnd"),
|
||||
EVENT_STALLED("onPlaybackStalled"),
|
||||
EVENT_RESUME("onPlaybackResume"),
|
||||
EVENT_READY_FOR_DISPLAY("onReadyForDisplay"),
|
||||
EVENT_FULLSCREEN_WILL_PRESENT("onVideoFullscreenPlayerWillPresent"),
|
||||
EVENT_FULLSCREEN_DID_PRESENT("onVideoFullscreenPlayerDidPresent"),
|
||||
EVENT_FULLSCREEN_WILL_DISMISS("onVideoFullscreenPlayerWillDismiss"),
|
||||
EVENT_FULLSCREEN_DID_DISMISS("onVideoFullscreenPlayerDidDismiss");
|
||||
|
||||
private final String mName;
|
||||
|
||||
Events(final String name) {
|
||||
mName = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return mName;
|
||||
}
|
||||
}
|
||||
|
||||
public static final String EVENT_PROP_FAST_FORWARD = "canPlayFastForward";
|
||||
public static final String EVENT_PROP_SLOW_FORWARD = "canPlaySlowForward";
|
||||
public static final String EVENT_PROP_SLOW_REVERSE = "canPlaySlowReverse";
|
||||
public static final String EVENT_PROP_REVERSE = "canPlayReverse";
|
||||
public static final String EVENT_PROP_STEP_FORWARD = "canStepForward";
|
||||
public static final String EVENT_PROP_STEP_BACKWARD = "canStepBackward";
|
||||
|
||||
public static final String EVENT_PROP_DURATION = "duration";
|
||||
public static final String EVENT_PROP_PLAYABLE_DURATION = "playableDuration";
|
||||
public static final String EVENT_PROP_SEEKABLE_DURATION = "seekableDuration";
|
||||
public static final String EVENT_PROP_CURRENT_TIME = "currentTime";
|
||||
public static final String EVENT_PROP_SEEK_TIME = "seekTime";
|
||||
public static final String EVENT_PROP_NATURALSIZE = "naturalSize";
|
||||
public static final String EVENT_PROP_WIDTH = "width";
|
||||
public static final String EVENT_PROP_HEIGHT = "height";
|
||||
public static final String EVENT_PROP_ORIENTATION = "orientation";
|
||||
public static final String EVENT_PROP_METADATA = "metadata";
|
||||
public static final String EVENT_PROP_TARGET = "target";
|
||||
public static final String EVENT_PROP_METADATA_IDENTIFIER = "identifier";
|
||||
public static final String EVENT_PROP_METADATA_VALUE = "value";
|
||||
|
||||
public static final String EVENT_PROP_ERROR = "error";
|
||||
public static final String EVENT_PROP_WHAT = "what";
|
||||
public static final String EVENT_PROP_EXTRA = "extra";
|
||||
|
||||
private ThemedReactContext mThemedReactContext;
|
||||
private RCTEventEmitter mEventEmitter;
|
||||
|
||||
private Handler mProgressUpdateHandler = new Handler();
|
||||
private Runnable mProgressUpdateRunnable = null;
|
||||
private Handler videoControlHandler = new Handler();
|
||||
private MediaController mediaController;
|
||||
|
||||
private String mSrcUriString = null;
|
||||
private String mSrcType = "mp4";
|
||||
private ReadableMap mRequestHeaders = null;
|
||||
private boolean mSrcIsNetwork = false;
|
||||
private boolean mSrcIsAsset = false;
|
||||
private ScalableType mResizeMode = ScalableType.LEFT_TOP;
|
||||
private boolean mRepeat = false;
|
||||
private boolean mPaused = false;
|
||||
private boolean mMuted = false;
|
||||
private boolean mPreventsDisplaySleepDuringVideoPlayback = true;
|
||||
private float mVolume = 1.0f;
|
||||
private float mStereoPan = 0.0f;
|
||||
private float mProgressUpdateInterval = 250.0f;
|
||||
private float mRate = 1.0f;
|
||||
private float mActiveRate = 1.0f;
|
||||
private long mSeekTime = 0;
|
||||
private boolean mPlayInBackground = false;
|
||||
private boolean mBackgroundPaused = false;
|
||||
private boolean mIsFullscreen = 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;
|
||||
private boolean mUseNativeControls = false;
|
||||
|
||||
public ReactVideoView(ThemedReactContext themedReactContext) {
|
||||
super(themedReactContext);
|
||||
|
||||
mThemedReactContext = themedReactContext;
|
||||
mEventEmitter = themedReactContext.getJSModule(RCTEventEmitter.class);
|
||||
themedReactContext.addLifecycleEventListener(this);
|
||||
|
||||
initializeMediaPlayerIfNeeded();
|
||||
setSurfaceTextureListener(this);
|
||||
|
||||
mProgressUpdateRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
if (mMediaPlayerValid && !isCompleted && !mPaused && !mBackgroundPaused) {
|
||||
WritableMap event = Arguments.createMap();
|
||||
event.putDouble(EVENT_PROP_CURRENT_TIME, mMediaPlayer.getCurrentPosition() / 1000.0);
|
||||
event.putDouble(EVENT_PROP_PLAYABLE_DURATION, mVideoBufferedDuration / 1000.0); //TODO:mBufferUpdateRunnable
|
||||
event.putDouble(EVENT_PROP_SEEKABLE_DURATION, mVideoDuration / 1000.0);
|
||||
mEventEmitter.receiveEvent(getId(), Events.EVENT_PROGRESS.toString(), event);
|
||||
|
||||
// Check for update after an interval
|
||||
mProgressUpdateHandler.postDelayed(mProgressUpdateRunnable, Math.round(mProgressUpdateInterval));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
if (mUseNativeControls) {
|
||||
initializeMediaControllerIfNeeded();
|
||||
mediaController.show();
|
||||
}
|
||||
|
||||
return super.onTouchEvent(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressLint("DrawAllocation")
|
||||
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
||||
super.onLayout(changed, left, top, right, bottom);
|
||||
|
||||
if (!changed || !mMediaPlayerValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
int videoWidth = getVideoWidth();
|
||||
int videoHeight = getVideoHeight();
|
||||
|
||||
if (videoWidth == 0 || videoHeight == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
Size viewSize = new Size(getWidth(), getHeight());
|
||||
Size videoSize = new Size(videoWidth, videoHeight);
|
||||
ScaleManager scaleManager = new ScaleManager(viewSize, videoSize);
|
||||
Matrix matrix = scaleManager.getScaleMatrix(mScalableType);
|
||||
if (matrix != null) {
|
||||
setTransform(matrix);
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeMediaPlayerIfNeeded() {
|
||||
if (mMediaPlayer == null) {
|
||||
mMediaPlayerValid = false;
|
||||
mMediaPlayer = new MediaPlayer();
|
||||
mMediaPlayer.setOnVideoSizeChangedListener(this);
|
||||
mMediaPlayer.setOnErrorListener(this);
|
||||
mMediaPlayer.setOnPreparedListener(this);
|
||||
mMediaPlayer.setOnBufferingUpdateListener(this);
|
||||
mMediaPlayer.setOnSeekCompleteListener(this);
|
||||
mMediaPlayer.setOnCompletionListener(this);
|
||||
mMediaPlayer.setOnInfoListener(this);
|
||||
if (Build.VERSION.SDK_INT >= 23) {
|
||||
mMediaPlayer.setOnTimedMetaDataAvailableListener(new TimedMetaDataAvailableListener());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeMediaControllerIfNeeded() {
|
||||
if (mediaController == null) {
|
||||
mediaController = new MediaController(this.getContext());
|
||||
}
|
||||
}
|
||||
|
||||
public void cleanupMediaPlayerResources() {
|
||||
if ( mediaController != null ) {
|
||||
mediaController.hide();
|
||||
}
|
||||
if ( mMediaPlayer != null ) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
mMediaPlayer.setOnTimedMetaDataAvailableListener(null);
|
||||
}
|
||||
mMediaPlayerValid = false;
|
||||
release();
|
||||
}
|
||||
if (mIsFullscreen) {
|
||||
setFullscreen(false);
|
||||
}
|
||||
if (mThemedReactContext != null) {
|
||||
mThemedReactContext.removeLifecycleEventListener(this);
|
||||
mThemedReactContext = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void setSrc(final String uriString, final String type, final boolean isNetwork, final boolean isAsset, final ReadableMap requestHeaders) {
|
||||
setSrc(uriString, type, isNetwork, isAsset, requestHeaders, 0, 0);
|
||||
}
|
||||
|
||||
public void setSrc(final String uriString, final String type, final boolean isNetwork, final boolean isAsset, final ReadableMap requestHeaders, final int expansionMainVersion, final int expansionPatchVersion) {
|
||||
|
||||
mSrcUriString = uriString;
|
||||
mSrcType = type;
|
||||
mSrcIsNetwork = isNetwork;
|
||||
mSrcIsAsset = isAsset;
|
||||
mRequestHeaders = requestHeaders;
|
||||
mMainVer = expansionMainVersion;
|
||||
mPatchVer = expansionPatchVersion;
|
||||
|
||||
|
||||
mMediaPlayerValid = false;
|
||||
mVideoDuration = 0;
|
||||
mVideoBufferedDuration = 0;
|
||||
|
||||
initializeMediaPlayerIfNeeded();
|
||||
mMediaPlayer.reset();
|
||||
|
||||
try {
|
||||
if (isNetwork) {
|
||||
// Use the shared CookieManager to access the cookies
|
||||
// set by WebViews inside the same app
|
||||
CookieManager cookieManager = CookieManager.getInstance();
|
||||
|
||||
Uri parsedUrl = Uri.parse(uriString);
|
||||
Uri.Builder builtUrl = parsedUrl.buildUpon();
|
||||
|
||||
String cookie = cookieManager.getCookie(builtUrl.build().toString());
|
||||
|
||||
Map<String, String> headers = new HashMap<String, String>();
|
||||
|
||||
if (cookie != null) {
|
||||
headers.put("Cookie", cookie);
|
||||
}
|
||||
|
||||
if (mRequestHeaders != null) {
|
||||
headers.putAll(toStringMap(mRequestHeaders));
|
||||
}
|
||||
|
||||
/* According to https://github.com/react-native-community/react-native-video/pull/537
|
||||
* there is an issue with this where it can cause a IOException.
|
||||
* TODO: diagnose this exception and fix it
|
||||
*/
|
||||
setDataSource(mThemedReactContext, parsedUrl, headers);
|
||||
} else if (isAsset) {
|
||||
if (uriString.startsWith("content://")) {
|
||||
Uri parsedUrl = Uri.parse(uriString);
|
||||
setDataSource(mThemedReactContext, parsedUrl);
|
||||
} else {
|
||||
setDataSource(uriString);
|
||||
}
|
||||
} else {
|
||||
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) {
|
||||
int identifier = mThemedReactContext.getResources().getIdentifier(
|
||||
uriString,
|
||||
"drawable",
|
||||
mThemedReactContext.getPackageName()
|
||||
);
|
||||
if (identifier == 0) {
|
||||
identifier = mThemedReactContext.getResources().getIdentifier(
|
||||
uriString,
|
||||
"raw",
|
||||
mThemedReactContext.getPackageName()
|
||||
);
|
||||
}
|
||||
setRawData(identifier);
|
||||
}
|
||||
else {
|
||||
setDataSource(fd.getFileDescriptor(), fd.getStartOffset(),fd.getLength());
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return;
|
||||
}
|
||||
|
||||
WritableMap src = Arguments.createMap();
|
||||
|
||||
WritableMap wRequestHeaders = Arguments.createMap();
|
||||
wRequestHeaders.merge(mRequestHeaders);
|
||||
|
||||
src.putString(ReactVideoViewManager.PROP_SRC_URI, uriString);
|
||||
src.putString(ReactVideoViewManager.PROP_SRC_TYPE, type);
|
||||
src.putMap(ReactVideoViewManager.PROP_SRC_HEADERS, wRequestHeaders);
|
||||
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);
|
||||
isCompleted = false;
|
||||
|
||||
try {
|
||||
prepareAsync(this);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public void setResizeModeModifier(final ScalableType resizeMode) {
|
||||
mResizeMode = resizeMode;
|
||||
|
||||
if (mMediaPlayerValid) {
|
||||
setScalableType(resizeMode);
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public void setRepeatModifier(final boolean repeat) {
|
||||
|
||||
mRepeat = repeat;
|
||||
|
||||
if (mMediaPlayerValid) {
|
||||
setLooping(repeat);
|
||||
}
|
||||
}
|
||||
|
||||
public void setPausedModifier(final boolean paused) {
|
||||
mPaused = paused;
|
||||
|
||||
if (!mMediaPlayerValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mPaused) {
|
||||
if (mMediaPlayer.isPlaying()) {
|
||||
pause();
|
||||
}
|
||||
} else {
|
||||
if (!mMediaPlayer.isPlaying()) {
|
||||
start();
|
||||
// Setting the rate unpauses, so we have to wait for an unpause
|
||||
if (mRate != mActiveRate) {
|
||||
setRateModifier(mRate);
|
||||
}
|
||||
|
||||
// Also Start the Progress Update Handler
|
||||
mProgressUpdateHandler.post(mProgressUpdateRunnable);
|
||||
}
|
||||
}
|
||||
setKeepScreenOn(!mPaused && mPreventsDisplaySleepDuringVideoPlayback);
|
||||
}
|
||||
|
||||
// reduces the volume based on stereoPan
|
||||
private float calulateRelativeVolume() {
|
||||
float relativeVolume = (mVolume * (1 - Math.abs(mStereoPan)));
|
||||
// only one decimal allowed
|
||||
BigDecimal roundRelativeVolume = new BigDecimal(relativeVolume).setScale(1, BigDecimal.ROUND_HALF_UP);
|
||||
return roundRelativeVolume.floatValue();
|
||||
}
|
||||
|
||||
public void setPreventsDisplaySleepDuringVideoPlaybackModifier(final boolean preventsDisplaySleepDuringVideoPlayback) {
|
||||
mPreventsDisplaySleepDuringVideoPlayback = preventsDisplaySleepDuringVideoPlayback;
|
||||
|
||||
if (!mMediaPlayerValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
mMediaPlayer.setScreenOnWhilePlaying(mPreventsDisplaySleepDuringVideoPlayback);
|
||||
setKeepScreenOn(mPreventsDisplaySleepDuringVideoPlayback);
|
||||
}
|
||||
|
||||
public void setMutedModifier(final boolean muted) {
|
||||
mMuted = muted;
|
||||
|
||||
if (!mMediaPlayerValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mMuted) {
|
||||
setVolume(0, 0);
|
||||
} else if (mStereoPan < 0) {
|
||||
// louder on the left channel
|
||||
setVolume(mVolume, calulateRelativeVolume());
|
||||
} else if (mStereoPan > 0) {
|
||||
// louder on the right channel
|
||||
setVolume(calulateRelativeVolume(), mVolume);
|
||||
} else {
|
||||
// same volume on both channels
|
||||
setVolume(mVolume, mVolume);
|
||||
}
|
||||
}
|
||||
|
||||
public void setVolumeModifier(final float volume) {
|
||||
mVolume = volume;
|
||||
setMutedModifier(mMuted);
|
||||
}
|
||||
|
||||
public void setStereoPan(final float stereoPan) {
|
||||
mStereoPan = stereoPan;
|
||||
setMutedModifier(mMuted);
|
||||
}
|
||||
|
||||
public void setProgressUpdateInterval(final float progressUpdateInterval) {
|
||||
mProgressUpdateInterval = progressUpdateInterval;
|
||||
}
|
||||
|
||||
public void setRateModifier(final float rate) {
|
||||
mRate = rate;
|
||||
|
||||
if (mMediaPlayerValid) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
if (!mPaused) { // Applying the rate while paused will cause the video to start
|
||||
/* Per https://stackoverflow.com/questions/39442522/setplaybackparams-causes-illegalstateexception
|
||||
* Some devices throw an IllegalStateException if you set the rate without first calling reset()
|
||||
* TODO: Call reset() then reinitialize the player
|
||||
*/
|
||||
try {
|
||||
mMediaPlayer.setPlaybackParams(mMediaPlayer.getPlaybackParams().setSpeed(rate));
|
||||
mActiveRate = rate;
|
||||
} catch (Exception e) {
|
||||
Log.e(ReactVideoViewManager.REACT_CLASS, "Unable to set rate, unsupported on this device");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.e(ReactVideoViewManager.REACT_CLASS, "Setting playback rate is not yet supported on Android versions below 6.0");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setFullscreen(boolean isFullscreen) {
|
||||
if (isFullscreen == mIsFullscreen) {
|
||||
return; // Avoid generating events when nothing is changing
|
||||
}
|
||||
mIsFullscreen = isFullscreen;
|
||||
|
||||
Activity activity = mThemedReactContext.getCurrentActivity();
|
||||
if (activity == null) {
|
||||
return;
|
||||
}
|
||||
Window window = activity.getWindow();
|
||||
View decorView = window.getDecorView();
|
||||
int uiOptions;
|
||||
if (mIsFullscreen) {
|
||||
if (Build.VERSION.SDK_INT >= 19) { // 4.4+
|
||||
uiOptions = SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||
| SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||
| SYSTEM_UI_FLAG_FULLSCREEN;
|
||||
} else {
|
||||
uiOptions = SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||
| SYSTEM_UI_FLAG_FULLSCREEN;
|
||||
}
|
||||
mEventEmitter.receiveEvent(getId(), Events.EVENT_FULLSCREEN_WILL_PRESENT.toString(), null);
|
||||
decorView.setSystemUiVisibility(uiOptions);
|
||||
mEventEmitter.receiveEvent(getId(), Events.EVENT_FULLSCREEN_DID_PRESENT.toString(), null);
|
||||
} else {
|
||||
uiOptions = View.SYSTEM_UI_FLAG_VISIBLE;
|
||||
mEventEmitter.receiveEvent(getId(), Events.EVENT_FULLSCREEN_WILL_DISMISS.toString(), null);
|
||||
decorView.setSystemUiVisibility(uiOptions);
|
||||
mEventEmitter.receiveEvent(getId(), Events.EVENT_FULLSCREEN_DID_DISMISS.toString(), null);
|
||||
}
|
||||
}
|
||||
|
||||
public void applyModifiers() {
|
||||
setResizeModeModifier(mResizeMode);
|
||||
setRepeatModifier(mRepeat);
|
||||
setPausedModifier(mPaused);
|
||||
setMutedModifier(mMuted);
|
||||
setPreventsDisplaySleepDuringVideoPlaybackModifier(mPreventsDisplaySleepDuringVideoPlayback);
|
||||
setProgressUpdateInterval(mProgressUpdateInterval);
|
||||
setRateModifier(mRate);
|
||||
}
|
||||
|
||||
public void setPlayInBackground(final boolean playInBackground) {
|
||||
|
||||
mPlayInBackground = playInBackground;
|
||||
}
|
||||
|
||||
public void setControls(boolean controls) {
|
||||
this.mUseNativeControls = controls;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPrepared(MediaPlayer mp) {
|
||||
|
||||
mMediaPlayerValid = true;
|
||||
mVideoDuration = mp.getDuration();
|
||||
|
||||
WritableMap naturalSize = Arguments.createMap();
|
||||
naturalSize.putInt(EVENT_PROP_WIDTH, mp.getVideoWidth());
|
||||
naturalSize.putInt(EVENT_PROP_HEIGHT, mp.getVideoHeight());
|
||||
if (mp.getVideoWidth() > mp.getVideoHeight())
|
||||
naturalSize.putString(EVENT_PROP_ORIENTATION, "landscape");
|
||||
else
|
||||
naturalSize.putString(EVENT_PROP_ORIENTATION, "portrait");
|
||||
|
||||
WritableMap event = Arguments.createMap();
|
||||
event.putDouble(EVENT_PROP_DURATION, mVideoDuration / 1000.0);
|
||||
event.putDouble(EVENT_PROP_CURRENT_TIME, mp.getCurrentPosition() / 1000.0);
|
||||
event.putMap(EVENT_PROP_NATURALSIZE, naturalSize);
|
||||
// TODO: Actually check if you can.
|
||||
event.putBoolean(EVENT_PROP_FAST_FORWARD, true);
|
||||
event.putBoolean(EVENT_PROP_SLOW_FORWARD, true);
|
||||
event.putBoolean(EVENT_PROP_SLOW_REVERSE, true);
|
||||
event.putBoolean(EVENT_PROP_REVERSE, true);
|
||||
event.putBoolean(EVENT_PROP_FAST_FORWARD, true);
|
||||
event.putBoolean(EVENT_PROP_STEP_BACKWARD, true);
|
||||
event.putBoolean(EVENT_PROP_STEP_FORWARD, true);
|
||||
mEventEmitter.receiveEvent(getId(), Events.EVENT_LOAD.toString(), event);
|
||||
|
||||
applyModifiers();
|
||||
|
||||
if (mUseNativeControls) {
|
||||
initializeMediaControllerIfNeeded();
|
||||
mediaController.setMediaPlayer(this);
|
||||
mediaController.setAnchorView(this);
|
||||
|
||||
videoControlHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mediaController.setEnabled(true);
|
||||
mediaController.show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
selectTimedMetadataTrack(mp);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onError(MediaPlayer mp, int what, int extra) {
|
||||
|
||||
WritableMap error = Arguments.createMap();
|
||||
error.putInt(EVENT_PROP_WHAT, what);
|
||||
error.putInt(EVENT_PROP_EXTRA, extra);
|
||||
WritableMap event = Arguments.createMap();
|
||||
event.putMap(EVENT_PROP_ERROR, error);
|
||||
mEventEmitter.receiveEvent(getId(), Events.EVENT_ERROR.toString(), event);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onInfo(MediaPlayer mp, int what, int extra) {
|
||||
switch (what) {
|
||||
case MediaPlayer.MEDIA_INFO_BUFFERING_START:
|
||||
mEventEmitter.receiveEvent(getId(), Events.EVENT_STALLED.toString(), Arguments.createMap());
|
||||
break;
|
||||
case MediaPlayer.MEDIA_INFO_BUFFERING_END:
|
||||
mEventEmitter.receiveEvent(getId(), Events.EVENT_RESUME.toString(), Arguments.createMap());
|
||||
break;
|
||||
case MediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START:
|
||||
mEventEmitter.receiveEvent(getId(), Events.EVENT_READY_FOR_DISPLAY.toString(), Arguments.createMap());
|
||||
break;
|
||||
|
||||
default:
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBufferingUpdate(MediaPlayer mp, int percent) {
|
||||
selectTimedMetadataTrack(mp);
|
||||
mVideoBufferedDuration = (int) Math.round((double) (mVideoDuration * percent) / 100.0);
|
||||
}
|
||||
|
||||
public void onSeekComplete(MediaPlayer mp) {
|
||||
WritableMap event = Arguments.createMap();
|
||||
event.putDouble(EVENT_PROP_CURRENT_TIME, getCurrentPosition() / 1000.0);
|
||||
event.putDouble(EVENT_PROP_SEEK_TIME, mSeekTime / 1000.0);
|
||||
mEventEmitter.receiveEvent(getId(), Events.EVENT_SEEK.toString(), event);
|
||||
mSeekTime = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void seekTo(int msec) {
|
||||
if (mMediaPlayerValid) {
|
||||
mSeekTime = msec;
|
||||
mMediaPlayer.seekTo(msec, MediaPlayer.SEEK_CLOSEST);
|
||||
if (isCompleted && mVideoDuration != 0 && msec < mVideoDuration) {
|
||||
isCompleted = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBufferPercentage() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canPause() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canSeekBackward() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canSeekForward() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAudioSessionId() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCompletion(MediaPlayer mp) {
|
||||
isCompleted = true;
|
||||
mEventEmitter.receiveEvent(getId(), Events.EVENT_END.toString(), null);
|
||||
if (!mRepeat) {
|
||||
setKeepScreenOn(false);
|
||||
}
|
||||
}
|
||||
|
||||
// This is not fully tested and does not work for all forms of timed metadata
|
||||
@TargetApi(23) // 6.0
|
||||
public class TimedMetaDataAvailableListener
|
||||
implements MediaPlayer.OnTimedMetaDataAvailableListener
|
||||
{
|
||||
public void onTimedMetaDataAvailable(MediaPlayer mp, TimedMetaData data) {
|
||||
WritableMap event = Arguments.createMap();
|
||||
|
||||
try {
|
||||
String rawMeta = new String(data.getMetaData(), "UTF-8");
|
||||
WritableMap id3 = Arguments.createMap();
|
||||
|
||||
id3.putString(EVENT_PROP_METADATA_VALUE, rawMeta.substring(rawMeta.lastIndexOf("\u0003") + 1));
|
||||
id3.putString(EVENT_PROP_METADATA_IDENTIFIER, "id3/TDEN");
|
||||
|
||||
WritableArray metadata = new WritableNativeArray();
|
||||
|
||||
metadata.pushMap(id3);
|
||||
|
||||
event.putArray(EVENT_PROP_METADATA, metadata);
|
||||
event.putDouble(EVENT_PROP_TARGET, getId());
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
mEventEmitter.receiveEvent(getId(), Events.EVENT_TIMED_METADATA.toString(), event);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
mMediaPlayerValid = false;
|
||||
super.onDetachedFromWindow();
|
||||
setKeepScreenOn(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
|
||||
if(mMainVer>0) {
|
||||
setSrc(mSrcUriString, mSrcType, mSrcIsNetwork, mSrcIsAsset, mRequestHeaders, mMainVer, mPatchVer);
|
||||
}
|
||||
else {
|
||||
setSrc(mSrcUriString, mSrcType, mSrcIsNetwork, mSrcIsAsset, mRequestHeaders);
|
||||
}
|
||||
setKeepScreenOn(mPreventsDisplaySleepDuringVideoPlayback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHostPause() {
|
||||
if (mMediaPlayerValid && !mPaused && !mPlayInBackground) {
|
||||
/* Pause the video in background
|
||||
* Don't update the paused prop, developers should be able to update it on background
|
||||
* so that when you return to the app the video is paused
|
||||
*/
|
||||
mBackgroundPaused = true;
|
||||
mMediaPlayer.pause();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHostResume() {
|
||||
mBackgroundPaused = false;
|
||||
if (mMediaPlayerValid && !mPlayInBackground && !mPaused) {
|
||||
new Handler().post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// Restore original state
|
||||
setPausedModifier(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHostDestroy() {
|
||||
}
|
||||
|
||||
/**
|
||||
* toStringMap converts a {@link ReadableMap} into a HashMap.
|
||||
*
|
||||
* @param readableMap The ReadableMap to be conveted.
|
||||
* @return A HashMap containing the data that was in the ReadableMap.
|
||||
* @see 'Adapted from https://github.com/artemyarulin/react-native-eval/blob/master/android/src/main/java/com/evaluator/react/ConversionUtil.java'
|
||||
*/
|
||||
public static Map<String, String> toStringMap(@Nullable ReadableMap readableMap) {
|
||||
Map<String, String> result = new HashMap<>();
|
||||
if (readableMap == null)
|
||||
return result;
|
||||
|
||||
com.facebook.react.bridge.ReadableMapKeySetIterator iterator = readableMap.keySetIterator();
|
||||
while (iterator.hasNextKey()) {
|
||||
String key = iterator.nextKey();
|
||||
result.put(key, readableMap.getString(key));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Select track (so we can use it to listen to timed meta data updates)
|
||||
private void selectTimedMetadataTrack(MediaPlayer mp) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
return;
|
||||
}
|
||||
try { // It's possible this could throw an exception if the framework doesn't support getting track info
|
||||
MediaPlayer.TrackInfo[] trackInfo = mp.getTrackInfo();
|
||||
for (int i = 0; i < trackInfo.length; ++i) {
|
||||
if (trackInfo[i].getTrackType() == MediaPlayer.TrackInfo.MEDIA_TRACK_TYPE_TIMEDTEXT) {
|
||||
mp.selectTrack(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {}
|
||||
}
|
||||
}
|
@@ -1,172 +0,0 @@
|
||||
package com.brentvatne.react;
|
||||
|
||||
import com.brentvatne.react.ReactVideoView.Events;
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.common.MapBuilder;
|
||||
import com.facebook.react.uimanager.annotations.ReactProp;
|
||||
import com.facebook.react.uimanager.SimpleViewManager;
|
||||
import com.facebook.react.uimanager.ThemedReactContext;
|
||||
import com.facebook.react.uimanager.events.RCTEventEmitter;
|
||||
import com.yqritc.scalablevideoview.ScalableType;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Map;
|
||||
|
||||
public class ReactVideoViewManager extends SimpleViewManager<ReactVideoView> {
|
||||
|
||||
public static final String REACT_CLASS = "RCTVideo";
|
||||
|
||||
public static final String PROP_SRC = "src";
|
||||
public static final String PROP_SRC_URI = "uri";
|
||||
public static final String PROP_SRC_TYPE = "type";
|
||||
public static final String PROP_SRC_HEADERS = "requestHeaders";
|
||||
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";
|
||||
public static final String PROP_PAUSED = "paused";
|
||||
public static final String PROP_MUTED = "muted";
|
||||
public static final String PROP_PREVENTS_DISPLAY_SLEEP_DURING_VIDEO_PLAYBACK = "preventsDisplaySleepDuringVideoPlayback";
|
||||
public static final String PROP_VOLUME = "volume";
|
||||
public static final String PROP_STEREO_PAN = "stereoPan";
|
||||
public static final String PROP_PROGRESS_UPDATE_INTERVAL = "progressUpdateInterval";
|
||||
public static final String PROP_SEEK = "seek";
|
||||
public static final String PROP_RATE = "rate";
|
||||
public static final String PROP_FULLSCREEN = "fullscreen";
|
||||
public static final String PROP_PLAY_IN_BACKGROUND = "playInBackground";
|
||||
public static final String PROP_CONTROLS = "controls";
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return REACT_CLASS;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ReactVideoView createViewInstance(ThemedReactContext themedReactContext) {
|
||||
return new ReactVideoView(themedReactContext);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDropViewInstance(ReactVideoView view) {
|
||||
super.onDropViewInstance(view);
|
||||
view.cleanupMediaPlayerResources();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Map getExportedCustomDirectEventTypeConstants() {
|
||||
MapBuilder.Builder builder = MapBuilder.builder();
|
||||
for (Events event : Events.values()) {
|
||||
builder.put(event.toString(), MapBuilder.of("registrationName", event.toString()));
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Map getExportedViewConstants() {
|
||||
return MapBuilder.of(
|
||||
"ScaleNone", Integer.toString(ScalableType.LEFT_TOP.ordinal()),
|
||||
"ScaleToFill", Integer.toString(ScalableType.FIT_XY.ordinal()),
|
||||
"ScaleAspectFit", Integer.toString(ScalableType.FIT_CENTER.ordinal()),
|
||||
"ScaleAspectFill", Integer.toString(ScalableType.CENTER_CROP.ordinal())
|
||||
);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_SRC)
|
||||
public void setSrc(final ReactVideoView videoView, @Nullable ReadableMap src) {
|
||||
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),
|
||||
src.getMap(PROP_SRC_HEADERS),
|
||||
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),
|
||||
src.getMap(PROP_SRC_HEADERS)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_PREVENTS_DISPLAY_SLEEP_DURING_VIDEO_PLAYBACK)
|
||||
public void setPropPreventsDisplaySleepDuringVideoPlayback(final ReactVideoView videoView, final boolean doPreventSleep) {
|
||||
videoView.setPreventsDisplaySleepDuringVideoPlaybackModifier(doPreventSleep);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_RESIZE_MODE)
|
||||
public void setResizeMode(final ReactVideoView videoView, final String resizeModeOrdinalString) {
|
||||
videoView.setResizeModeModifier(ScalableType.values()[Integer.parseInt(resizeModeOrdinalString)]);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_REPEAT, defaultBoolean = false)
|
||||
public void setRepeat(final ReactVideoView videoView, final boolean repeat) {
|
||||
videoView.setRepeatModifier(repeat);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_PAUSED, defaultBoolean = false)
|
||||
public void setPaused(final ReactVideoView videoView, final boolean paused) {
|
||||
videoView.setPausedModifier(paused);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_MUTED, defaultBoolean = false)
|
||||
public void setMuted(final ReactVideoView videoView, final boolean muted) {
|
||||
videoView.setMutedModifier(muted);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_VOLUME, defaultFloat = 1.0f)
|
||||
public void setVolume(final ReactVideoView videoView, final float volume) {
|
||||
videoView.setVolumeModifier(volume);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_STEREO_PAN)
|
||||
public void setStereoPan(final ReactVideoView videoView, final float stereoPan) {
|
||||
videoView.setStereoPan(stereoPan);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_PROGRESS_UPDATE_INTERVAL, defaultFloat = 250.0f)
|
||||
public void setProgressUpdateInterval(final ReactVideoView videoView, final float progressUpdateInterval) {
|
||||
videoView.setProgressUpdateInterval(progressUpdateInterval);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_SEEK)
|
||||
public void setSeek(final ReactVideoView videoView, final float seek) {
|
||||
videoView.seekTo(Math.round(seek * 1000.0f));
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_RATE)
|
||||
public void setRate(final ReactVideoView videoView, final float rate) {
|
||||
videoView.setRateModifier(rate);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_FULLSCREEN, defaultBoolean = false)
|
||||
public void setFullscreen(final ReactVideoView videoView, final boolean fullscreen) {
|
||||
videoView.setFullscreen(fullscreen);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_PLAY_IN_BACKGROUND, defaultBoolean = false)
|
||||
public void setPlayInBackground(final ReactVideoView videoView, final boolean playInBackground) {
|
||||
videoView.setPlayInBackground(playInBackground);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_CONTROLS, defaultBoolean = false)
|
||||
public void setControls(final ReactVideoView videoView, final boolean controls) {
|
||||
videoView.setControls(controls);
|
||||
}
|
||||
}
|
@@ -0,0 +1,39 @@
|
||||
package com.brentvatne.receiver;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.media.AudioManager;
|
||||
|
||||
public class AudioBecomingNoisyReceiver extends BroadcastReceiver {
|
||||
|
||||
private final Context context;
|
||||
private BecomingNoisyListener listener = BecomingNoisyListener.NO_OP;
|
||||
|
||||
public AudioBecomingNoisyReceiver(Context context) {
|
||||
this.context = context.getApplicationContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (AudioManager.ACTION_AUDIO_BECOMING_NOISY.equals(intent.getAction())) {
|
||||
listener.onAudioBecomingNoisy();
|
||||
}
|
||||
}
|
||||
|
||||
public void setListener(BecomingNoisyListener listener) {
|
||||
this.listener = listener;
|
||||
IntentFilter intentFilter = new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY);
|
||||
context.registerReceiver(this, intentFilter);
|
||||
}
|
||||
|
||||
public void removeListener() {
|
||||
this.listener = BecomingNoisyListener.NO_OP;
|
||||
try {
|
||||
context.unregisterReceiver(this);
|
||||
} catch (Exception ignore) {
|
||||
// ignore if already unregistered
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
package com.brentvatne.receiver;
|
||||
|
||||
public interface BecomingNoisyListener {
|
||||
|
||||
BecomingNoisyListener NO_OP = new BecomingNoisyListener() {
|
||||
@Override public void onAudioBecomingNoisy() {
|
||||
// NO_OP
|
||||
}
|
||||
};
|
||||
|
||||
void onAudioBecomingNoisy();
|
||||
|
||||
}
|
76
android/src/main/res/layout/exo_player_control_view.xml
Normal file
76
android/src/main/res/layout/exo_player_control_view.xml
Normal file
@@ -0,0 +1,76 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom"
|
||||
android:layoutDirection="ltr"
|
||||
android:background="#CC000000"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:paddingTop="4dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageButton android:id="@id/exo_prev"
|
||||
style="@style/ExoMediaButton.Previous"/>
|
||||
|
||||
<ImageButton android:id="@id/exo_rew"
|
||||
style="@style/ExoMediaButton.Rewind"/>
|
||||
<FrameLayout
|
||||
android:id="@+id/exo_play_pause_container"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center">
|
||||
<ImageButton android:id="@id/exo_play"
|
||||
style="@style/ExoMediaButton.Play"/>
|
||||
|
||||
<ImageButton android:id="@id/exo_pause"
|
||||
style="@style/ExoMediaButton.Pause"/>
|
||||
</FrameLayout>
|
||||
|
||||
<ImageButton android:id="@id/exo_ffwd"
|
||||
style="@style/ExoMediaButton.FastForward"/>
|
||||
|
||||
<ImageButton android:id="@id/exo_next"
|
||||
style="@style/ExoMediaButton.Next"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView android:id="@id/exo_position"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
android:paddingLeft="4dp"
|
||||
android:paddingRight="4dp"
|
||||
android:includeFontPadding="false"
|
||||
android:textColor="#FFBEBEBE"/>
|
||||
|
||||
<com.google.android.exoplayer2.ui.DefaultTimeBar
|
||||
android:id="@id/exo_progress"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="26dp"/>
|
||||
|
||||
<TextView android:id="@id/exo_duration"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
android:paddingLeft="4dp"
|
||||
android:paddingRight="4dp"
|
||||
android:includeFontPadding="false"
|
||||
android:textColor="#FFBEBEBE"/>
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
19
android/src/main/res/values/strings.xml
Normal file
19
android/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
|
||||
<string name="error_no_decoder">This device does not provide a decoder for <xliff:g id="mime_type">%1$s</xliff:g></string>
|
||||
|
||||
<string name="error_no_secure_decoder">This device does not provide a secure decoder for <xliff:g id="mime_type">%1$s</xliff:g></string>
|
||||
|
||||
<string name="error_querying_decoders">Unable to query device decoders</string>
|
||||
|
||||
<string name="error_instantiating_decoder">Unable to instantiate decoder <xliff:g id="decoder_name">%1$s</xliff:g></string>
|
||||
|
||||
<string name="error_drm_not_supported">Protected content not supported on API levels below 18</string>
|
||||
|
||||
<string name="unrecognized_media_format">Unrecognized media format</string>
|
||||
|
||||
<string name="error_drm_unsupported_scheme">This device does not support the required DRM scheme</string>
|
||||
|
||||
<string name="error_drm_unknown">An unknown DRM error occurred</string>
|
||||
</resources>
|
Reference in New Issue
Block a user