blob: febb938fdbd1b739e196d0b7abb2900b0897a1fe [file] [log] [blame]
/*
* Copyright 2021 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.google.android.exoplayer2;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.LOCAL_VARIABLE;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.TYPE_USE;
import android.net.ConnectivityManager;
import android.os.Bundle;
import android.os.RemoteException;
import android.os.SystemClock;
import android.text.TextUtils;
import androidx.annotation.CallSuper;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.Util;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/** Thrown when a non locally recoverable playback failure occurs. */
public class PlaybackException extends Exception implements Bundleable {
/**
* Codes that identify causes of player errors.
*
* <p>This list of errors may be extended in future versions, and {@link Player} implementations
* may define custom error codes.
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target({FIELD, METHOD, PARAMETER, LOCAL_VARIABLE, TYPE_USE})
@IntDef(
open = true,
value = {
ERROR_CODE_UNSPECIFIED,
ERROR_CODE_REMOTE_ERROR,
ERROR_CODE_BEHIND_LIVE_WINDOW,
ERROR_CODE_TIMEOUT,
ERROR_CODE_FAILED_RUNTIME_CHECK,
ERROR_CODE_IO_UNSPECIFIED,
ERROR_CODE_IO_NETWORK_CONNECTION_FAILED,
ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT,
ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE,
ERROR_CODE_IO_BAD_HTTP_STATUS,
ERROR_CODE_IO_FILE_NOT_FOUND,
ERROR_CODE_IO_NO_PERMISSION,
ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED,
ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE,
ERROR_CODE_PARSING_CONTAINER_MALFORMED,
ERROR_CODE_PARSING_MANIFEST_MALFORMED,
ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED,
ERROR_CODE_PARSING_MANIFEST_UNSUPPORTED,
ERROR_CODE_DECODER_INIT_FAILED,
ERROR_CODE_DECODER_QUERY_FAILED,
ERROR_CODE_DECODING_FAILED,
ERROR_CODE_DECODING_FORMAT_EXCEEDS_CAPABILITIES,
ERROR_CODE_DECODING_FORMAT_UNSUPPORTED,
ERROR_CODE_AUDIO_TRACK_INIT_FAILED,
ERROR_CODE_AUDIO_TRACK_WRITE_FAILED,
ERROR_CODE_DRM_UNSPECIFIED,
ERROR_CODE_DRM_SCHEME_UNSUPPORTED,
ERROR_CODE_DRM_PROVISIONING_FAILED,
ERROR_CODE_DRM_CONTENT_ERROR,
ERROR_CODE_DRM_LICENSE_ACQUISITION_FAILED,
ERROR_CODE_DRM_DISALLOWED_OPERATION,
ERROR_CODE_DRM_SYSTEM_ERROR,
ERROR_CODE_DRM_DEVICE_REVOKED,
ERROR_CODE_DRM_LICENSE_EXPIRED
})
public @interface ErrorCode {}
// Miscellaneous errors (1xxx).
/** Caused by an error whose cause could not be identified. */
public static final int ERROR_CODE_UNSPECIFIED = 1000;
/**
* Caused by an unidentified error in a remote Player, which is a Player that runs on a different
* host or process.
*/
public static final int ERROR_CODE_REMOTE_ERROR = 1001;
/** Caused by the loading position falling behind the sliding window of available live content. */
public static final int ERROR_CODE_BEHIND_LIVE_WINDOW = 1002;
/** Caused by a generic timeout. */
public static final int ERROR_CODE_TIMEOUT = 1003;
/**
* Caused by a failed runtime check.
*
* <p>This can happen when the application fails to comply with the player's API requirements (for
* example, by passing invalid arguments), or when the player reaches an invalid state.
*/
public static final int ERROR_CODE_FAILED_RUNTIME_CHECK = 1004;
// Input/Output errors (2xxx).
/** Caused by an Input/Output error which could not be identified. */
public static final int ERROR_CODE_IO_UNSPECIFIED = 2000;
/**
* Caused by a network connection failure.
*
* <p>The following is a non-exhaustive list of possible reasons:
*
* <ul>
* <li>There is no network connectivity (you can check this by querying {@link
* ConnectivityManager#getActiveNetwork}).
* <li>The URL's domain is misspelled or does not exist.
* <li>The target host is unreachable.
* <li>The server unexpectedly closes the connection.
* </ul>
*/
public static final int ERROR_CODE_IO_NETWORK_CONNECTION_FAILED = 2001;
/** Caused by a network timeout, meaning the server is taking too long to fulfill a request. */
public static final int ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT = 2002;
/**
* Caused by a server returning a resource with an invalid "Content-Type" HTTP header value.
*
* <p>For example, this can happen when the player is expecting a piece of media, but the server
* returns a paywall HTML page, with content type "text/html".
*/
public static final int ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE = 2003;
/** Caused by an HTTP server returning an unexpected HTTP response status code. */
public static final int ERROR_CODE_IO_BAD_HTTP_STATUS = 2004;
/** Caused by a non-existent file. */
public static final int ERROR_CODE_IO_FILE_NOT_FOUND = 2005;
/**
* Caused by lack of permission to perform an IO operation. For example, lack of permission to
* access internet or external storage.
*/
public static final int ERROR_CODE_IO_NO_PERMISSION = 2006;
/**
* Caused by the player trying to access cleartext HTTP traffic (meaning http:// rather than
* https://) when the app's Network Security Configuration does not permit it.
*
* <p>See <a href="https://exoplayer.dev/issues/cleartext-not-permitted">this corresponding
* troubleshooting topic</a>.
*/
public static final int ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED = 2007;
/** Caused by reading data out of the data bound. */
public static final int ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE = 2008;
// Content parsing errors (3xxx).
/** Caused by a parsing error associated with a media container format bitstream. */
public static final int ERROR_CODE_PARSING_CONTAINER_MALFORMED = 3001;
/**
* Caused by a parsing error associated with a media manifest. Examples of a media manifest are a
* DASH or a SmoothStreaming manifest, or an HLS playlist.
*/
public static final int ERROR_CODE_PARSING_MANIFEST_MALFORMED = 3002;
/**
* Caused by attempting to extract a file with an unsupported media container format, or an
* unsupported media container feature.
*/
public static final int ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED = 3003;
/**
* Caused by an unsupported feature in a media manifest. Examples of a media manifest are a DASH
* or a SmoothStreaming manifest, or an HLS playlist.
*/
public static final int ERROR_CODE_PARSING_MANIFEST_UNSUPPORTED = 3004;
// Decoding errors (4xxx).
/** Caused by a decoder initialization failure. */
public static final int ERROR_CODE_DECODER_INIT_FAILED = 4001;
/** Caused by a decoder query failure. */
public static final int ERROR_CODE_DECODER_QUERY_FAILED = 4002;
/** Caused by a failure while trying to decode media samples. */
public static final int ERROR_CODE_DECODING_FAILED = 4003;
/** Caused by trying to decode content whose format exceeds the capabilities of the device. */
public static final int ERROR_CODE_DECODING_FORMAT_EXCEEDS_CAPABILITIES = 4004;
/** Caused by trying to decode content whose format is not supported. */
public static final int ERROR_CODE_DECODING_FORMAT_UNSUPPORTED = 4005;
// AudioTrack errors (5xxx).
/** Caused by an AudioTrack initialization failure. */
public static final int ERROR_CODE_AUDIO_TRACK_INIT_FAILED = 5001;
/** Caused by an AudioTrack write operation failure. */
public static final int ERROR_CODE_AUDIO_TRACK_WRITE_FAILED = 5002;
// DRM errors (6xxx).
/** Caused by an unspecified error related to DRM protection. */
public static final int ERROR_CODE_DRM_UNSPECIFIED = 6000;
/**
* Caused by a chosen DRM protection scheme not being supported by the device. Examples of DRM
* protection schemes are ClearKey and Widevine.
*/
public static final int ERROR_CODE_DRM_SCHEME_UNSUPPORTED = 6001;
/** Caused by a failure while provisioning the device. */
public static final int ERROR_CODE_DRM_PROVISIONING_FAILED = 6002;
/**
* Caused by attempting to play incompatible DRM-protected content.
*
* <p>For example, this can happen when attempting to play a DRM protected stream using a scheme
* (like Widevine) for which there is no corresponding license acquisition data (like a pssh box).
*/
public static final int ERROR_CODE_DRM_CONTENT_ERROR = 6003;
/** Caused by a failure while trying to obtain a license. */
public static final int ERROR_CODE_DRM_LICENSE_ACQUISITION_FAILED = 6004;
/** Caused by an operation being disallowed by a license policy. */
public static final int ERROR_CODE_DRM_DISALLOWED_OPERATION = 6005;
/** Caused by an error in the DRM system. */
public static final int ERROR_CODE_DRM_SYSTEM_ERROR = 6006;
/** Caused by the device having revoked DRM privileges. */
public static final int ERROR_CODE_DRM_DEVICE_REVOKED = 6007;
/** Caused by an expired DRM license being loaded into an open DRM session. */
public static final int ERROR_CODE_DRM_LICENSE_EXPIRED = 6008;
/**
* Player implementations that want to surface custom errors can use error codes greater than this
* value, so as to avoid collision with other error codes defined in this class.
*/
public static final int CUSTOM_ERROR_CODE_BASE = 1000000;
/** Returns the name of a given {@code errorCode}. */
public static String getErrorCodeName(@ErrorCode int errorCode) {
switch (errorCode) {
case ERROR_CODE_UNSPECIFIED:
return "ERROR_CODE_UNSPECIFIED";
case ERROR_CODE_REMOTE_ERROR:
return "ERROR_CODE_REMOTE_ERROR";
case ERROR_CODE_BEHIND_LIVE_WINDOW:
return "ERROR_CODE_BEHIND_LIVE_WINDOW";
case ERROR_CODE_TIMEOUT:
return "ERROR_CODE_TIMEOUT";
case ERROR_CODE_FAILED_RUNTIME_CHECK:
return "ERROR_CODE_FAILED_RUNTIME_CHECK";
case ERROR_CODE_IO_UNSPECIFIED:
return "ERROR_CODE_IO_UNSPECIFIED";
case ERROR_CODE_IO_NETWORK_CONNECTION_FAILED:
return "ERROR_CODE_IO_NETWORK_CONNECTION_FAILED";
case ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT:
return "ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT";
case ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE:
return "ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE";
case ERROR_CODE_IO_BAD_HTTP_STATUS:
return "ERROR_CODE_IO_BAD_HTTP_STATUS";
case ERROR_CODE_IO_FILE_NOT_FOUND:
return "ERROR_CODE_IO_FILE_NOT_FOUND";
case ERROR_CODE_IO_NO_PERMISSION:
return "ERROR_CODE_IO_NO_PERMISSION";
case ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED:
return "ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED";
case ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE:
return "ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE";
case ERROR_CODE_PARSING_CONTAINER_MALFORMED:
return "ERROR_CODE_PARSING_CONTAINER_MALFORMED";
case ERROR_CODE_PARSING_MANIFEST_MALFORMED:
return "ERROR_CODE_PARSING_MANIFEST_MALFORMED";
case ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED:
return "ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED";
case ERROR_CODE_PARSING_MANIFEST_UNSUPPORTED:
return "ERROR_CODE_PARSING_MANIFEST_UNSUPPORTED";
case ERROR_CODE_DECODER_INIT_FAILED:
return "ERROR_CODE_DECODER_INIT_FAILED";
case ERROR_CODE_DECODER_QUERY_FAILED:
return "ERROR_CODE_DECODER_QUERY_FAILED";
case ERROR_CODE_DECODING_FAILED:
return "ERROR_CODE_DECODING_FAILED";
case ERROR_CODE_DECODING_FORMAT_EXCEEDS_CAPABILITIES:
return "ERROR_CODE_DECODING_FORMAT_EXCEEDS_CAPABILITIES";
case ERROR_CODE_DECODING_FORMAT_UNSUPPORTED:
return "ERROR_CODE_DECODING_FORMAT_UNSUPPORTED";
case ERROR_CODE_AUDIO_TRACK_INIT_FAILED:
return "ERROR_CODE_AUDIO_TRACK_INIT_FAILED";
case ERROR_CODE_AUDIO_TRACK_WRITE_FAILED:
return "ERROR_CODE_AUDIO_TRACK_WRITE_FAILED";
case ERROR_CODE_DRM_UNSPECIFIED:
return "ERROR_CODE_DRM_UNSPECIFIED";
case ERROR_CODE_DRM_SCHEME_UNSUPPORTED:
return "ERROR_CODE_DRM_SCHEME_UNSUPPORTED";
case ERROR_CODE_DRM_PROVISIONING_FAILED:
return "ERROR_CODE_DRM_PROVISIONING_FAILED";
case ERROR_CODE_DRM_CONTENT_ERROR:
return "ERROR_CODE_DRM_CONTENT_ERROR";
case ERROR_CODE_DRM_LICENSE_ACQUISITION_FAILED:
return "ERROR_CODE_DRM_LICENSE_ACQUISITION_FAILED";
case ERROR_CODE_DRM_DISALLOWED_OPERATION:
return "ERROR_CODE_DRM_DISALLOWED_OPERATION";
case ERROR_CODE_DRM_SYSTEM_ERROR:
return "ERROR_CODE_DRM_SYSTEM_ERROR";
case ERROR_CODE_DRM_DEVICE_REVOKED:
return "ERROR_CODE_DRM_DEVICE_REVOKED";
case ERROR_CODE_DRM_LICENSE_EXPIRED:
return "ERROR_CODE_DRM_LICENSE_EXPIRED";
default:
if (errorCode >= CUSTOM_ERROR_CODE_BASE) {
return "custom error code";
} else {
return "invalid error code";
}
}
}
/**
* Equivalent to {@link PlaybackException#getErrorCodeName(int)
* PlaybackException.getErrorCodeName(this.errorCode)}.
*/
public final String getErrorCodeName() {
return getErrorCodeName(errorCode);
}
/** An error code which identifies the cause of the playback failure. */
public final @ErrorCode int errorCode;
/** The value of {@link SystemClock#elapsedRealtime()} when this exception was created. */
public final long timestampMs;
/**
* Creates an instance.
*
* @param errorCode A number which identifies the cause of the error. May be one of the {@link
* ErrorCode ErrorCodes}.
* @param cause See {@link #getCause()}.
* @param message See {@link #getMessage()}.
*/
public PlaybackException(
@Nullable String message, @Nullable Throwable cause, @ErrorCode int errorCode) {
this(message, cause, errorCode, Clock.DEFAULT.elapsedRealtime());
}
/** Creates a new instance using the fields obtained from the given {@link Bundle}. */
protected PlaybackException(Bundle bundle) {
this(
/* message= */ bundle.getString(keyForField(FIELD_STRING_MESSAGE)),
/* cause= */ getCauseFromBundle(bundle),
/* errorCode= */ bundle.getInt(
keyForField(FIELD_INT_ERROR_CODE), /* defaultValue= */ ERROR_CODE_UNSPECIFIED),
/* timestampMs= */ bundle.getLong(
keyForField(FIELD_LONG_TIMESTAMP_MS),
/* defaultValue= */ SystemClock.elapsedRealtime()));
}
/** Creates a new instance using the given values. */
protected PlaybackException(
@Nullable String message,
@Nullable Throwable cause,
@ErrorCode int errorCode,
long timestampMs) {
super(message, cause);
this.errorCode = errorCode;
this.timestampMs = timestampMs;
}
/**
* Returns whether the error data associated to this exception equals the error data associated to
* {@code other}.
*
* <p>Note that this method does not compare the exceptions' stacktraces.
*/
@CallSuper
public boolean errorInfoEquals(@Nullable PlaybackException other) {
if (this == other) {
return true;
}
if (other == null || getClass() != other.getClass()) {
return false;
}
@Nullable Throwable thisCause = getCause();
@Nullable Throwable thatCause = other.getCause();
if (thisCause != null && thatCause != null) {
if (!Util.areEqual(thisCause.getMessage(), thatCause.getMessage())) {
return false;
}
if (!Util.areEqual(thisCause.getClass(), thatCause.getClass())) {
return false;
}
} else if (thisCause != null || thatCause != null) {
return false;
}
return errorCode == other.errorCode
&& Util.areEqual(getMessage(), other.getMessage())
&& timestampMs == other.timestampMs;
}
// Bundleable implementation.
/**
* Identifiers for fields in a {@link Bundle} which represents a playback exception. Subclasses
* may use {@link #FIELD_CUSTOM_ID_BASE} to generate more keys using {@link #keyForField(int)}.
*
* <p>Note: Changes to the Bundleable implementation must be backwards compatible, so as to avoid
* breaking communication across different Bundleable implementation versions.
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef(
open = true,
value = {
FIELD_INT_ERROR_CODE,
FIELD_LONG_TIMESTAMP_MS,
FIELD_STRING_MESSAGE,
FIELD_STRING_CAUSE_CLASS_NAME,
FIELD_STRING_CAUSE_MESSAGE,
})
protected @interface FieldNumber {}
private static final int FIELD_INT_ERROR_CODE = 0;
private static final int FIELD_LONG_TIMESTAMP_MS = 1;
private static final int FIELD_STRING_MESSAGE = 2;
private static final int FIELD_STRING_CAUSE_CLASS_NAME = 3;
private static final int FIELD_STRING_CAUSE_MESSAGE = 4;
/**
* Defines a minimum field id value for subclasses to use when implementing {@link #toBundle()}
* and {@link Bundleable.Creator}.
*
* <p>Subclasses should obtain their {@link Bundle Bundle's} field keys by applying a non-negative
* offset on this constant and passing the result to {@link #keyForField(int)}.
*/
protected static final int FIELD_CUSTOM_ID_BASE = 1000;
/** Object that can create a {@link PlaybackException} from a {@link Bundle}. */
public static final Creator<PlaybackException> CREATOR = PlaybackException::new;
@CallSuper
@Override
public Bundle toBundle() {
Bundle bundle = new Bundle();
bundle.putInt(keyForField(FIELD_INT_ERROR_CODE), errorCode);
bundle.putLong(keyForField(FIELD_LONG_TIMESTAMP_MS), timestampMs);
bundle.putString(keyForField(FIELD_STRING_MESSAGE), getMessage());
@Nullable Throwable cause = getCause();
if (cause != null) {
bundle.putString(keyForField(FIELD_STRING_CAUSE_CLASS_NAME), cause.getClass().getName());
bundle.putString(keyForField(FIELD_STRING_CAUSE_MESSAGE), cause.getMessage());
}
return bundle;
}
/**
* Converts the given {@link FieldNumber} to a string which can be used as a field key when
* implementing {@link #toBundle()} and {@link Bundleable.Creator}.
*/
protected static String keyForField(@FieldNumber int field) {
return Integer.toString(field, Character.MAX_RADIX);
}
// Creates a new {@link Throwable} with possibly {@code null} message.
@SuppressWarnings("nullness:argument")
private static Throwable createThrowable(Class<?> clazz, @Nullable String message)
throws Exception {
return (Throwable) clazz.getConstructor(String.class).newInstance(message);
}
// Creates a new {@link RemoteException} with possibly {@code null} message.
@SuppressWarnings("nullness:argument")
private static RemoteException createRemoteException(@Nullable String message) {
return new RemoteException(message);
}
@Nullable
private static Throwable getCauseFromBundle(Bundle bundle) {
@Nullable String causeClassName = bundle.getString(keyForField(FIELD_STRING_CAUSE_CLASS_NAME));
@Nullable String causeMessage = bundle.getString(keyForField(FIELD_STRING_CAUSE_MESSAGE));
@Nullable Throwable cause = null;
if (!TextUtils.isEmpty(causeClassName)) {
try {
Class<?> clazz =
Class.forName(
causeClassName, /* initialize= */ true, PlaybackException.class.getClassLoader());
if (Throwable.class.isAssignableFrom(clazz)) {
cause = createThrowable(clazz, causeMessage);
}
} catch (Throwable e) {
// There was an error while creating the cause using reflection, do nothing here and let the
// finally block handle the issue.
} finally {
if (cause == null) {
// The bundle has fields to represent the cause, but we were unable to re-create the
// exception using reflection. We instantiate a RemoteException to reflect this problem.
cause = createRemoteException(causeMessage);
}
}
}
return cause;
}
}