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:
olivier bouillet
2022-06-23 21:58:11 +02:00
41 changed files with 148 additions and 2472 deletions

46
android/README.md Normal file
View File

@@ -0,0 +1,46 @@
## react-native-video - ExoPlayer
This is an Android React Native video component based on ExoPlayer v2.
> ExoPlayer is an application level media player for Android. It provides an alternative to Androids MediaPlayer API for playing audio and video both locally and over the Internet. ExoPlayer supports features not currently supported by Androids MediaPlayer API, including DASH and SmoothStreaming adaptive playbacks. Unlike the MediaPlayer API, ExoPlayer is easy to customize and extend, and can be updated through Play Store application updates.
https://github.com/google/ExoPlayer
## Benefits over `react-native-video@0.9.0`:
- Android Video library built by Google, with a lot of support
- Supports DASH, HLS, & SmoothStreaming adaptive streams
- Supports formats such as MP4, M4A, FMP4, WebM, MKV, MP3, Ogg, WAV, MPEG-TS, MPEG-PS, FLV and ADTS (AAC).
- Fewer device specific issues
- Highly customisable
## ExoPlayer only props
```javascript
render() {
return (
<Video
...
disableFocus={true} // disables audio focus and wake lock (default false)
onAudioBecomingNoisy={this.onAudioBecomingNoisy} // Callback when audio is becoming noisy - should pause video
onAudioFocusChanged={this.onAudioFocusChanged} // Callback when audio focus has been lost - pause if focus has been lost
/>
)
}
onAudioBecomingNoisy = () => {
this.setState({ pause: true })
}
onAudioFocusChanged = (event: { hasAudioFocus: boolean }) => {
if (!this.state.paused && !event.hasAudioFocus) {
this.setState({ paused: true })
}
}
```
## Unimplemented props
- Expansion file - `source={{ mainVer: 1, patchVer: 0 }}`

View File

@@ -8,19 +8,43 @@ android {
compileSdkVersion safeExtGet('compileSdkVersion', 31)
buildToolsVersion safeExtGet('buildToolsVersion', '30.0.2')
compileOptions {
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
minSdkVersion safeExtGet('minSdkVersion', 21)
targetSdkVersion safeExtGet('targetSdkVersion', 28)
versionCode 1
versionName "1.0"
ndk {
abiFilters "armeabi-v7a", "x86"
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
//noinspection GradleDynamicVersion
implementation "com.facebook.react:react-native:${safeExtGet('reactNativeVersion', '+')}"
implementation 'com.github.thang2162:Android-ScalableVideoView:v1.1.1'
repositories {
// Remove this repository line after google releases to google() or mavenCentral()
maven { url "https://dl.google.com/android/maven2" }
}
dependencies {
implementation "com.facebook.react:react-native:${safeExtGet('reactNativeVersion', '+')}"
implementation('com.google.android.exoplayer:exoplayer:2.17.1') {
exclude group: 'com.android.support'
}
// All support libs must use the same version
implementation "androidx.annotation:annotation:1.1.0"
implementation "androidx.core:core:1.1.0"
implementation "androidx.media:media:1.1.0"
implementation('com.google.android.exoplayer:extension-okhttp:2.17.1') {
exclude group: 'com.squareup.okhttp3', module: 'okhttp'
}
implementation 'com.squareup.okhttp3:okhttp:${OKHTTP_VERSION}'
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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");
}
}
}

View File

@@ -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));
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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.
}
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}

View File

@@ -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));
}
}

View File

@@ -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) {}
}
}

View File

@@ -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);
}
}

View File

@@ -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
}
}
}

View File

@@ -0,0 +1,13 @@
package com.brentvatne.receiver;
public interface BecomingNoisyListener {
BecomingNoisyListener NO_OP = new BecomingNoisyListener() {
@Override public void onAudioBecomingNoisy() {
// NO_OP
}
};
void onAudioBecomingNoisy();
}

View 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>

View 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>