Commit 3ebf94cd by olly Committed by Oliver Woodman

DataSources: Remove position-out-of-range workarounds

PiperOrigin-RevId: 364871094
parent 2b099563
......@@ -31,6 +31,12 @@
* Analytics:
* Add `onAudioCodecError` and `onVideoCodecError` to `AnalyticsListener`.
* Downloads and caching:
* Fix `CacheWriter` to correctly handle cases where the request `DataSpec`
extends beyond the end of the underlying resource. Caching will now
succeed in this case, with data up to the end of the resource being
cached. This behaviour is enabled by default, and so the
`allowShortContent` parameter has been removed
([#7326](https://github.com/google/ExoPlayer/issues/7326)).
* Fix `CacheWriter` to correctly handle `DataSource.close` failures, for
which it cannot be assumed that data was successfully written to the
cache.
......
......@@ -630,22 +630,6 @@ public final class CacheDataSource implements DataSource {
return read(buffer, offset, readLength);
}
return bytesRead;
} catch (IOException e) {
// TODO: This is not correct, because position-out-of-range exceptions should only be thrown
// if the requested position is more than one byte beyond the end of the resource. Conversely,
// this code is assuming that a position-out-of-range exception indicates the requested
// position is exactly one byte beyond the end of the resource, which is not a case for which
// this type of exception should be thrown. This exception handling may be required for
// interop with current HttpDataSource implementations that do (incorrectly) throw a
// position-out-of-range exception at this position. It should be removed when the
// HttpDataSource implementations are fixed.
if (currentDataSpec.length == C.LENGTH_UNSET
&& DataSourceException.isCausedByPositionOutOfRange(e)) {
setNoBytesRemainingAndMaybeStoreLength(castNonNull(requestDataSpec.key));
return C.RESULT_END_OF_INPUT;
}
handleBeforeThrow(e);
throw e;
} catch (Throwable e) {
handleBeforeThrow(e);
throw e;
......
......@@ -18,7 +18,6 @@ package com.google.android.exoplayer2.upstream.cache;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.upstream.DataSourceException;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.util.PriorityTaskManager;
import com.google.android.exoplayer2.util.PriorityTaskManager.PriorityTooLowException;
......@@ -160,17 +159,6 @@ public final class CacheWriter {
isDataSourceOpen = true;
} catch (IOException e) {
Util.closeQuietly(dataSource);
// TODO: This exception handling may be required for interop with current HttpDataSource
// implementations that (incorrectly) throw a position-out-of-range when opened exactly one
// byte beyond the end of the resource. It should be removed when the HttpDataSource
// implementations are fixed.
if (isLastBlock && DataSourceException.isCausedByPositionOutOfRange(e)) {
// The length of the request exceeds the length of the content. If we allow shorter
// content and are reading the last block, fall through and try again with an unbounded
// request to read up to the end of the content.
} else {
throw e;
}
}
}
......
/*
* Copyright (C) 2018 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.video.surfacecapturer;
import android.graphics.Bitmap;
import android.graphics.SurfaceTexture;
import android.os.Handler;
import android.view.PixelCopy;
import android.view.Surface;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.google.android.exoplayer2.util.EGLSurfaceTexture;
/**
* A {@link SurfaceCapturer} implementation that uses {@link PixelCopy} APIs to perform image copy
* from a {@link SurfaceTexture} into a {@link Bitmap}.
*/
@RequiresApi(24)
/* package */ final class PixelCopySurfaceCapturerV24 extends SurfaceCapturer
implements EGLSurfaceTexture.TextureImageListener, PixelCopy.OnPixelCopyFinishedListener {
/** Exception to be thrown if there is some problem capturing images from the surface. */
public static final class SurfaceCapturerException extends Exception {
/**
* One of the {@link PixelCopy} {@code ERROR_*} values return from the {@link
* PixelCopy#request(Surface, Bitmap, PixelCopy.OnPixelCopyFinishedListener, Handler)}
*/
public final int errorCode;
/**
* Constructs a new instance.
*
* @param message The error message.
* @param errorCode The error code.
*/
public SurfaceCapturerException(String message, int errorCode) {
super(message);
this.errorCode = errorCode;
}
}
private final EGLSurfaceTexture eglSurfaceTexture;
private final Handler handler;
private final Surface decoderSurface;
@Nullable private Bitmap bitmap;
@SuppressWarnings("nullness")
/* package */ PixelCopySurfaceCapturerV24(
Callback callback, int outputWidth, int outputHeight, Handler imageRenderingHandler) {
super(callback, outputWidth, outputHeight);
this.handler = imageRenderingHandler;
eglSurfaceTexture = new EGLSurfaceTexture(imageRenderingHandler, /* callback= */ this);
eglSurfaceTexture.init(EGLSurfaceTexture.SECURE_MODE_NONE);
decoderSurface = new Surface(eglSurfaceTexture.getSurfaceTexture());
}
@Override
public Surface getSurface() {
return decoderSurface;
}
@Override
public void release() {
eglSurfaceTexture.release();
decoderSurface.release();
}
/** @see SurfaceTexture#setDefaultBufferSize(int, int) */
public void setDefaultSurfaceTextureBufferSize(int width, int height) {
eglSurfaceTexture.getSurfaceTexture().setDefaultBufferSize(width, height);
}
// TextureImageListener
@Override
public void onFrameAvailable() {
bitmap = Bitmap.createBitmap(getOutputWidth(), getOutputHeight(), Bitmap.Config.ARGB_8888);
PixelCopy.request(decoderSurface, bitmap, this, handler);
}
// OnPixelCopyFinishedListener
@Override
public void onPixelCopyFinished(int copyResult) {
Callback callback = getCallback();
if (copyResult == PixelCopy.SUCCESS && bitmap != null) {
callback.onSurfaceCaptured(bitmap);
} else {
callback.onSurfaceCaptureError(
new SurfaceCapturerException("Couldn't copy image from surface", copyResult));
}
}
}
/*
* Copyright (C) 2018 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.video.surfacecapturer;
import android.content.Context;
import android.media.MediaCodec;
import android.view.Surface;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.mediacodec.MediaCodecAdapter;
import com.google.android.exoplayer2.mediacodec.MediaCodecSelector;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import com.google.android.exoplayer2.video.MediaCodecVideoRenderer;
import java.nio.ByteBuffer;
/**
* Decodes and renders video using {@link MediaCodec}.
*
* <p>This video renderer will only render the first frame after position reset (seeking), or after
* being re-enabled.
*/
public class SingleFrameMediaCodecVideoRenderer extends MediaCodecVideoRenderer {
private static final String TAG = "SingleFrameMediaCodecVideoRenderer";
private boolean hasRenderedFirstFrame;
@Nullable private Surface surface;
public SingleFrameMediaCodecVideoRenderer(
Context context, MediaCodecSelector mediaCodecSelector) {
super(context, mediaCodecSelector);
}
@Override
public String getName() {
return TAG;
}
@Override
public void handleMessage(int messageType, @Nullable Object message) throws ExoPlaybackException {
if (messageType == MSG_SET_SURFACE) {
this.surface = (Surface) message;
}
super.handleMessage(messageType, message);
}
@Override
protected void onEnabled(boolean joining, boolean mayRenderStartOfStream)
throws ExoPlaybackException {
hasRenderedFirstFrame = false;
super.onEnabled(joining, mayRenderStartOfStream);
}
@Override
protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {
hasRenderedFirstFrame = false;
super.onPositionReset(positionUs, joining);
}
@Override
protected boolean processOutputBuffer(
long positionUs,
long elapsedRealtimeUs,
@Nullable MediaCodecAdapter codec,
@Nullable ByteBuffer buffer,
int bufferIndex,
int bufferFlags,
int sampleCount,
long bufferPresentationTimeUs,
boolean isDecodeOnlyBuffer,
boolean isLastBuffer,
Format format)
throws ExoPlaybackException {
Assertions.checkNotNull(codec); // Can not render video without codec
long presentationTimeUs = bufferPresentationTimeUs - getOutputStreamOffsetUs();
if (isDecodeOnlyBuffer && !isLastBuffer) {
skipOutputBuffer(codec, bufferIndex, presentationTimeUs);
return true;
}
if (surface == null || hasRenderedFirstFrame) {
return false;
}
hasRenderedFirstFrame = true;
if (Util.SDK_INT >= 21) {
renderOutputBufferV21(codec, bufferIndex, presentationTimeUs, System.nanoTime());
} else {
renderOutputBuffer(codec, bufferIndex, presentationTimeUs);
}
return true;
}
}
/*
* Copyright (C) 2018 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.video.surfacecapturer;
import android.graphics.Bitmap;
import android.view.Surface;
/**
* A surface capturer, which captures image drawn into its surface as bitmaps.
*
* <p>It constructs a {@link Surface}, which can be used as the output surface for an image producer
* to draw images to. As images are being drawn into this surface, this capturer will capture these
* images, and return them via {@link Callback}. The output images will have a fixed frame size of
* (width, height), and any image drawn into the surface will be stretched to fit this frame size.
*/
public abstract class SurfaceCapturer {
/** The callback to be notified of the image capturing result. */
public interface Callback {
/**
* Called when the surface capturer has been able to capture its surface into a {@link Bitmap}.
* This will happen whenever the producer updates the image on the wrapped surface.
*/
void onSurfaceCaptured(Bitmap bitmap);
/** Called when the surface capturer couldn't capture its surface due to an error. */
void onSurfaceCaptureError(Exception e);
}
/** The callback to be notified of the image capturing result. */
private final Callback callback;
/** The width of the output images. */
private final int outputWidth;
/** The height of the output images. */
private final int outputHeight;
/**
* Constructs a new instance.
*
* @param callback See {@link #callback}.
* @param outputWidth See {@link #outputWidth}.
* @param outputHeight See {@link #outputHeight}.
*/
protected SurfaceCapturer(Callback callback, int outputWidth, int outputHeight) {
this.callback = callback;
this.outputWidth = outputWidth;
this.outputHeight = outputHeight;
}
/** Returns the callback to be notified of the image capturing result. */
protected Callback getCallback() {
return callback;
}
/** Returns the width of the output images. */
public int getOutputWidth() {
return outputWidth;
}
/** Returns the height of the output images. */
public int getOutputHeight() {
return outputHeight;
}
/** Returns a {@link Surface} that image producers (camera, video codec etc...) can draw to. */
public abstract Surface getSurface();
/** Releases all kept resources. This instance cannot be used after this call. */
public abstract void release();
}
/*
* Copyright (C) 2018 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.video.surfacecapturer;
import android.graphics.Bitmap;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Message;
import android.view.Surface;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Renderer;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
/**
* A capturer that can capture the output of a video {@link SingleFrameMediaCodecVideoRenderer} into
* bitmaps.
*
* <p>Start by setting the output size via {@link #setOutputSize(int, int)}. The capturer will
* create a surface and set this up as the output for the video renderer.
*
* <p>Once the surface setup is done, the capturer will call {@link Callback#onOutputSizeSet(int,
* int)}. After this call, the capturer will capture all images rendered by the {@link Renderer},
* and deliver the captured bitmaps via {@link Callback#onSurfaceCaptured(Bitmap)}, or failure via
* {@link Callback#onSurfaceCaptureError(Exception)}. You can change the output image size at any
* time by calling {@link #setOutputSize(int, int)}.
*
* <p>When this capturer is no longer needed, you need to call {@link #release()} to release all
* resources it is holding. After this call returns, no callback will be called anymore.
*/
public final class VideoRendererOutputCapturer implements Handler.Callback {
/** The callback to be notified of the video image capturing result. */
public interface Callback extends SurfaceCapturer.Callback {
/** Called when output surface has been set properly. */
void onOutputSizeSet(int width, int height);
}
private static final int MSG_SET_OUTPUT = 1;
private static final int MSG_RELEASE = 2;
private final HandlerThread handlerThread;
private final Handler handler;
private final ExoPlayer exoPlayer;
private final EventDispatcher eventDispatcher;
private final Renderer renderer;
@Nullable private SurfaceCapturer surfaceCapturer;
private volatile boolean released;
/**
* Constructs a new instance.
*
* @param callback The callback to be notified of image capturing result.
* @param callbackHandler The {@link Handler} that the callback will be called on.
* @param videoRenderer A {@link SingleFrameMediaCodecVideoRenderer} that will be used to render
* video frames, which this capturer will capture.
* @param exoPlayer The {@link ExoPlayer} instance that is using the video renderer.
*/
public VideoRendererOutputCapturer(
Callback callback,
Handler callbackHandler,
SingleFrameMediaCodecVideoRenderer videoRenderer,
ExoPlayer exoPlayer) {
this.renderer = Assertions.checkNotNull(videoRenderer);
this.exoPlayer = Assertions.checkNotNull(exoPlayer);
this.eventDispatcher = new EventDispatcher(callbackHandler, callback);
// Use a separate thread to handle all operations in this class, because bitmap copying may take
// time and should not be handled on callbackHandler (which maybe run on main thread).
handlerThread = new HandlerThread("ExoPlayer:VideoRendererOutputCapturer");
handlerThread.start();
handler = Util.createHandler(handlerThread.getLooper(), /* callback= */ this);
}
/**
* Sets the size of the video renderer surface's with and height.
*
* <p>This call is performed asynchronously. Only after the {@code callback} receives a call to
* {@link Callback#onOutputSizeSet(int, int)}, the output frames will conform to the new size.
* Output frames before the callback will still conform to previous size.
*
* @param width The target width of the output frame.
* @param height The target height of the output frame.
*/
public void setOutputSize(int width, int height) {
handler.obtainMessage(MSG_SET_OUTPUT, width, height).sendToTarget();
}
/** Releases all kept resources. This instance cannot be used after this call. */
public synchronized void release() {
if (released) {
return;
}
// Some long running or waiting operations may run on the handler thread, so we try to
// interrupt the thread to end these operations quickly.
handlerThread.interrupt();
handler.removeCallbacksAndMessages(null);
handler.sendEmptyMessage(MSG_RELEASE);
boolean wasInterrupted = false;
while (!released) {
try {
wait();
} catch (InterruptedException e) {
wasInterrupted = true;
}
}
if (wasInterrupted) {
// Restore the interrupted status.
Thread.currentThread().interrupt();
}
}
// Handler.Callback
@Override
public boolean handleMessage(Message message) {
switch (message.what) {
case MSG_SET_OUTPUT:
handleSetOutput(/* width= */ message.arg1, /* height= */ message.arg2);
return true;
case MSG_RELEASE:
handleRelease();
return true;
default:
return false;
}
}
// Internal methods
private void handleSetOutput(int width, int height) {
if (surfaceCapturer == null
|| surfaceCapturer.getOutputWidth() != width
|| surfaceCapturer.getOutputHeight() != height) {
updateSurfaceCapturer(width, height);
}
eventDispatcher.onOutputSizeSet(width, height);
}
private void updateSurfaceCapturer(int width, int height) {
SurfaceCapturer oldSurfaceCapturer = surfaceCapturer;
if (oldSurfaceCapturer != null) {
blockingSetRendererSurface(/* surface= */ null);
oldSurfaceCapturer.release();
}
surfaceCapturer = createSurfaceCapturer(width, height);
blockingSetRendererSurface(surfaceCapturer.getSurface());
}
private SurfaceCapturer createSurfaceCapturer(int width, int height) {
if (Util.SDK_INT >= 24) {
return createSurfaceCapturerV24(width, height);
} else {
// TODO: Use different SurfaceCapturer based on API level, flags etc...
throw new UnsupportedOperationException(
"Creating Surface Capturer is not supported for API < 24 yet");
}
}
@RequiresApi(24)
private SurfaceCapturer createSurfaceCapturerV24(int width, int height) {
return new PixelCopySurfaceCapturerV24(eventDispatcher, width, height, handler);
}
private void blockingSetRendererSurface(@Nullable Surface surface) {
try {
exoPlayer
.createMessage(renderer)
.setType(Renderer.MSG_SET_SURFACE)
.setPayload(surface)
.send()
.blockUntilDelivered();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
private void handleRelease() {
eventDispatcher.release();
handler.removeCallbacksAndMessages(null);
if (surfaceCapturer != null) {
surfaceCapturer.release();
}
handlerThread.quit();
synchronized (this) {
released = true;
notifyAll();
}
}
/** Dispatches {@link Callback} events using a callback handler. */
private static final class EventDispatcher implements Callback {
private final Handler callbackHandler;
private final Callback callback;
private volatile boolean released;
private EventDispatcher(Handler callbackHandler, Callback callback) {
this.callbackHandler = callbackHandler;
this.callback = callback;
}
@Override
public void onOutputSizeSet(int width, int height) {
callbackHandler.post(
() -> {
if (released) {
return;
}
callback.onOutputSizeSet(width, height);
});
}
@Override
public void onSurfaceCaptured(Bitmap bitmap) {
callbackHandler.post(
() -> {
if (released) {
return;
}
callback.onSurfaceCaptured(bitmap);
});
}
@Override
public void onSurfaceCaptureError(Exception exception) {
callbackHandler.post(
() -> {
if (released) {
return;
}
callback.onSurfaceCaptureError(exception);
});
}
/** Releases this event dispatcher. No event will be dispatched after this call. */
public void release() {
released = true;
}
}
}
/*
* Copyright (C) 2019 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.
*/
@NonNullApi
package com.google.android.exoplayer2.video.surfacecapturer;
import com.google.android.exoplayer2.util.NonNullApi;
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment